merge main
@@ -90,10 +90,14 @@
|
||||
}
|
||||
|
||||
if (selectedProjectsToDelete?.length) {
|
||||
const projectsDeletionPromises = selectedProjectsToDelete.map((projectId) => ({
|
||||
projectId,
|
||||
promise: sdk.forConsole.projects.delete({ projectId })
|
||||
}));
|
||||
const projectsDeletionPromises = selectedProjectsToDelete.map((projectId) => {
|
||||
const projectToDelete = projects.find((project) => project.$id === projectId);
|
||||
|
||||
return {
|
||||
projectId,
|
||||
promise: sdk.forProject(projectToDelete.region, projectId).project.delete()
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
|
||||
@@ -112,8 +112,8 @@ export const defaultScopes: string[] = [
|
||||
'projects.write',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'executions.read',
|
||||
'executions.write',
|
||||
'targets.read',
|
||||
'targets.write',
|
||||
'subscribers.write',
|
||||
@@ -339,17 +339,31 @@ export const scopes: ScopeDefinition[] = [
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'execution.read',
|
||||
scope: 'executions.read',
|
||||
description: "Access to read your project's execution logs",
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'execution.write',
|
||||
scope: 'executions.write',
|
||||
description: "Access to execute your project's functions",
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'execution.read',
|
||||
description: "Access to read your project's execution logs",
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt',
|
||||
deprecated: true
|
||||
},
|
||||
{
|
||||
scope: 'execution.write',
|
||||
description: "Access to execute your project's functions",
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt',
|
||||
deprecated: true
|
||||
},
|
||||
{
|
||||
scope: 'targets.read',
|
||||
description: "Access to read your project's messaging targets",
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import type { Component } from 'svelte';
|
||||
import Apple from '$routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte';
|
||||
import Auth0 from '$routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte';
|
||||
import Authentik from '$routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte';
|
||||
import GitLab from '$routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte';
|
||||
import Google from '$routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte';
|
||||
import Main from '$routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte';
|
||||
import Microsoft from '$routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte';
|
||||
import Oidc from '$routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte';
|
||||
import Okta from '$routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte';
|
||||
|
||||
export type Provider = {
|
||||
name: string;
|
||||
@@ -28,19 +20,19 @@ export const oAuthProviders: Record<string, Provider> = {
|
||||
name: 'Apple',
|
||||
icon: 'apple',
|
||||
docs: 'https://developer.apple.com/sign-in-with-apple/',
|
||||
component: Apple
|
||||
component: Main
|
||||
},
|
||||
auth0: {
|
||||
name: 'Auth0',
|
||||
icon: 'auth0',
|
||||
docs: 'https://auth0.com/developers',
|
||||
component: Auth0
|
||||
component: Main
|
||||
},
|
||||
authentik: {
|
||||
name: 'Authentik',
|
||||
icon: 'authentik',
|
||||
docs: 'https://goauthentik.io/integrations/sources/oauth/',
|
||||
component: Authentik
|
||||
component: Main
|
||||
},
|
||||
autodesk: {
|
||||
name: 'Autodesk',
|
||||
@@ -108,6 +100,12 @@ export const oAuthProviders: Record<string, Provider> = {
|
||||
docs: 'https://www.figma.com/developers/api#access-tokens',
|
||||
component: Main
|
||||
},
|
||||
fusionauth: {
|
||||
name: 'FusionAuth',
|
||||
icon: 'fusionauth',
|
||||
docs: 'https://fusionauth.io/docs/apis/identity-providers/oauth2',
|
||||
component: Main
|
||||
},
|
||||
github: {
|
||||
name: 'GitHub',
|
||||
icon: 'github',
|
||||
@@ -125,21 +123,33 @@ export const oAuthProviders: Record<string, Provider> = {
|
||||
name: 'GitLab',
|
||||
icon: 'gitlab',
|
||||
docs: 'https://docs.gitlab.com/ee/api/',
|
||||
component: GitLab
|
||||
component: Main
|
||||
},
|
||||
google: {
|
||||
name: 'Google',
|
||||
icon: 'google',
|
||||
docs: 'https://support.google.com/googleapi/answer/6158849',
|
||||
component: Google
|
||||
component: Main
|
||||
},
|
||||
googleImagine: {
|
||||
name: 'Google',
|
||||
icon: 'google',
|
||||
docs: 'https://support.google.com/googleapi/answer/6158849',
|
||||
component: Google,
|
||||
component: Main,
|
||||
internal: true
|
||||
},
|
||||
keycloak: {
|
||||
name: 'Keycloak',
|
||||
icon: 'keycloak',
|
||||
docs: 'https://www.keycloak.org/securing-apps/oidc-layers',
|
||||
component: Main
|
||||
},
|
||||
kick: {
|
||||
name: 'Kick',
|
||||
icon: 'kick',
|
||||
docs: 'https://docs.kick.com/getting-started/using-the-api',
|
||||
component: Main
|
||||
},
|
||||
linkedin: {
|
||||
name: 'LinkedIn',
|
||||
icon: 'linkedin',
|
||||
@@ -150,7 +160,7 @@ export const oAuthProviders: Record<string, Provider> = {
|
||||
name: 'Microsoft',
|
||||
icon: 'microsoft',
|
||||
docs: 'https://developer.microsoft.com/en-us/',
|
||||
component: Microsoft
|
||||
component: Main
|
||||
},
|
||||
notion: {
|
||||
name: 'Notion',
|
||||
@@ -162,13 +172,13 @@ export const oAuthProviders: Record<string, Provider> = {
|
||||
name: 'OIDC',
|
||||
icon: 'oidc',
|
||||
docs: 'https://openid.net/connect/faq/',
|
||||
component: Oidc
|
||||
component: Main
|
||||
},
|
||||
okta: {
|
||||
name: 'Okta',
|
||||
icon: 'okta',
|
||||
docs: 'https://developer.okta.com',
|
||||
component: Okta
|
||||
component: Main
|
||||
},
|
||||
paypal: {
|
||||
name: 'Paypal',
|
||||
@@ -176,7 +186,7 @@ export const oAuthProviders: Record<string, Provider> = {
|
||||
docs: 'https://developer.paypal.com/docs/api/overview/',
|
||||
component: Main
|
||||
},
|
||||
paypalsandbox: {
|
||||
paypalSandbox: {
|
||||
name: 'Paypal Sandbox',
|
||||
icon: 'paypal',
|
||||
docs: 'https://developer.paypal.com/docs/api/overview/',
|
||||
@@ -248,12 +258,6 @@ export const oAuthProviders: Record<string, Provider> = {
|
||||
docs: 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
|
||||
component: Main
|
||||
},
|
||||
yammer: {
|
||||
name: 'Yammer',
|
||||
icon: 'yammer',
|
||||
docs: 'https://developer.yammer.com/docs/oauth-2',
|
||||
component: Main
|
||||
},
|
||||
yandex: {
|
||||
name: 'Yandex',
|
||||
icon: 'yandex',
|
||||
|
||||
@@ -148,6 +148,52 @@
|
||||
];
|
||||
}
|
||||
|
||||
function createBandwidthProgressData(
|
||||
totalBytes: number,
|
||||
realtimeBytes: number,
|
||||
maxGB: number
|
||||
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
|
||||
if (maxGB <= 0) return [];
|
||||
|
||||
const maxBytes = maxGB * 1000 * 1000 * 1000;
|
||||
const percentage = Math.min((totalBytes / maxBytes) * 100, 100);
|
||||
const showRealtimeSegment = realtimeBytes > 0 && realtimeBytes <= totalBytes;
|
||||
const realtimePortion = showRealtimeSegment ? realtimeBytes : 0;
|
||||
const regularPortion = Math.max(0, totalBytes - realtimePortion);
|
||||
const regularSize = humanFileSize(regularPortion);
|
||||
const realtimeSize = humanFileSize(realtimePortion);
|
||||
|
||||
const segments: Array<{
|
||||
size: number;
|
||||
color: string;
|
||||
tooltip?: { title: string; label: string };
|
||||
}> = [];
|
||||
|
||||
if (regularPortion > 0) {
|
||||
segments.push({
|
||||
size: regularPortion,
|
||||
color: 'var(--bgcolor-neutral-invert)',
|
||||
tooltip: {
|
||||
title: `${percentage.toFixed(0)}% used`,
|
||||
label: `Bandwidth: ${regularSize.value} ${regularSize.unit}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (realtimePortion > 0) {
|
||||
segments.push({
|
||||
size: realtimePortion,
|
||||
color: 'var(--bgcolor-neutral-invert-weak)',
|
||||
tooltip: {
|
||||
title: 'Realtime bandwidth',
|
||||
label: `${realtimeSize.value} ${realtimeSize.unit}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function getResource(resources: Array<Models.UsageResources> | undefined, resourceId: string) {
|
||||
return resources?.find((resource) => resource.resourceId === resourceId);
|
||||
}
|
||||
@@ -278,9 +324,13 @@
|
||||
const projects = (currentAggregation?.breakdown || []).map((projectData) => {
|
||||
const resources = projectData.resources || [];
|
||||
const bandwidth = getResource(resources, 'bandwidth');
|
||||
const realtimeBandwidth = getResource(resources, 'realtimeBandwidth');
|
||||
const storage = getResource(resources, 'storage');
|
||||
const authPhone = getResource(resources, 'authPhone');
|
||||
|
||||
const bandwidthValue = bandwidth?.value || 0;
|
||||
const realtimeBandwidthValue = realtimeBandwidth?.value || 0;
|
||||
|
||||
return {
|
||||
id: `project-${projectData.$id}`,
|
||||
expandable: true,
|
||||
@@ -305,10 +355,31 @@
|
||||
),
|
||||
priceFormatter: ({ amount }) => formatCurrency(amount),
|
||||
progressFactory: ({ value, planLimit, hasLimit }) =>
|
||||
hasLimit ? createStorageProgressData(value, planLimit || 0) : [],
|
||||
hasLimit
|
||||
? createBandwidthProgressData(
|
||||
value,
|
||||
realtimeBandwidthValue,
|
||||
planLimit || 0
|
||||
)
|
||||
: [],
|
||||
maxFactory: ({ planLimit, hasLimit }) =>
|
||||
hasLimit ? (planLimit || 0) * 1000 * 1000 * 1000 : null
|
||||
}),
|
||||
...(realtimeBandwidthValue > bandwidthValue
|
||||
? [
|
||||
createRow({
|
||||
id: 'realtime-bandwidth',
|
||||
label: 'Realtime bandwidth',
|
||||
resource: realtimeBandwidth,
|
||||
usageFormatter: ({ value }) => {
|
||||
const size = humanFileSize(value);
|
||||
return `${size.value} ${size.unit}`;
|
||||
},
|
||||
priceFormatter: ({ amount }) => formatCurrency(amount),
|
||||
includeProgress: false
|
||||
})
|
||||
]
|
||||
: []),
|
||||
// standard resources (numeric)
|
||||
createResourceRow(
|
||||
'users',
|
||||
@@ -379,15 +450,6 @@
|
||||
getResource(resources, 'realtimeMessages'),
|
||||
currentPlan?.realtimeMessages
|
||||
),
|
||||
createRow({
|
||||
id: 'realtime-bandwidth',
|
||||
label: 'Realtime bandwidth',
|
||||
resource: getResource(resources, 'realtimeBandwidth'),
|
||||
usageFormatter: ({ value }) =>
|
||||
humanFileSize(value).value + humanFileSize(value).unit,
|
||||
priceFormatter: ({ amount }) => formatCurrency(amount),
|
||||
includeProgress: false
|
||||
}),
|
||||
createRow({
|
||||
id: 'sms',
|
||||
label: 'Phone OTP',
|
||||
|
||||
@@ -584,7 +584,7 @@
|
||||
color: '#68A3FE',
|
||||
tooltip: {
|
||||
title: 'Backups storage',
|
||||
label: `${Math.round(bytesToSize(data.organizationUsage.backupsStorageTotal, 'GB') * 100) / 100}MB`
|
||||
label: `${Math.round(bytesToSize(data.organizationUsage.backupsStorageTotal, 'GB') * 100) / 100}GB`
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputSwitch, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import { Link, Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let keyID: string = null;
|
||||
let teamID: string = null;
|
||||
let p8: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ keyID, teamID, p8 } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret = keyID && teamID && p8 ? JSON.stringify({ keyID, teamID, p8 }) : provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} onSubmit={update} bind:show on:close title={`${provider.name} OAuth2 settings`}>
|
||||
<p slot="description">
|
||||
To use {provider.name} 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'} />
|
||||
<InputText
|
||||
id="servicesID"
|
||||
label="Services ID"
|
||||
autofocus={true}
|
||||
placeholder="com.company.appname"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputText id="keyID" label="Key ID" placeholder="SHAB13ROFN" bind:value={keyID} required />
|
||||
<InputText id="teamID" label="Team ID" placeholder="ELA2CD3AED" bind:value={teamID} required />
|
||||
<InputTextarea id="p8" label="P8 File" placeholder="" bind:value={p8} required />
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!keyID ||
|
||||
!teamID ||
|
||||
!p8 ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,91 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Link, Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let clientSecret: string = null;
|
||||
let auth0Domain: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, auth0Domain } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret =
|
||||
clientSecret && auth0Domain
|
||||
? JSON.stringify({ clientSecret, auth0Domain })
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
|
||||
<svelte:fragment slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more info
|
||||
you can
|
||||
<Link.Anchor href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</Link.Anchor>
|
||||
</svelte:fragment>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="clientID"
|
||||
label="Client ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="Client Secret"
|
||||
placeholder="Enter Client Secret"
|
||||
minlength={0}
|
||||
bind:value={clientSecret}
|
||||
required />
|
||||
<InputText
|
||||
id="domain"
|
||||
label="Auth0 Domain"
|
||||
placeholder="your-tenant.auth0.com"
|
||||
bind:value={auth0Domain}
|
||||
required />
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!clientSecret ||
|
||||
!auth0Domain ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,95 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Link, Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let clientSecret: string = null;
|
||||
let authentikDomain: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, authentikDomain } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret =
|
||||
clientSecret && authentikDomain
|
||||
? JSON.stringify({ clientSecret, authentikDomain })
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
|
||||
<p slot="description">
|
||||
To use {provider.name} 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'} />
|
||||
<InputText
|
||||
id="clientID"
|
||||
label="Client ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="Client Secret"
|
||||
placeholder="Enter Client Secret"
|
||||
minlength={0}
|
||||
bind:value={clientSecret}
|
||||
required />
|
||||
<InputText
|
||||
id="domain"
|
||||
label="Authentik Base-Domain"
|
||||
placeholder="Your Authentik domain"
|
||||
bind:value={authentikDomain}
|
||||
required />
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!clientSecret ||
|
||||
!authentikDomain ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,84 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Link, Alert } from '@appwrite.io/pink-svelte';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let clientSecret: string = null;
|
||||
let endpoint: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, endpoint } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret =
|
||||
clientSecret && endpoint ? JSON.stringify({ clientSecret, endpoint }) : provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
|
||||
<p slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more info
|
||||
you can
|
||||
<Link.Anchor href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</Link.Anchor>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="App ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="App Secret"
|
||||
placeholder="Enter App Secret"
|
||||
minlength={0}
|
||||
bind:value={clientSecret}
|
||||
required />
|
||||
<InputText id="endpoint" label="Endpoint" placeholder="Your endpoint" bind:value={endpoint} />
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!clientSecret ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,84 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let enabled: boolean = null;
|
||||
let appId: string = null;
|
||||
let secret: string = null;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
enabled ??= provider.enabled;
|
||||
appId ??= provider.appId;
|
||||
secret ??= provider.secret;
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
let error: string;
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal {error} bind:show onSubmit={update} title="{provider.name} OAuth2 settings" on:close>
|
||||
<svelte:fragment slot="title">{provider.name} OAuth2 settings</svelte:fragment>
|
||||
|
||||
<p slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more info
|
||||
you can
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="App ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="App Secret"
|
||||
placeholder="Enter App Secret"
|
||||
minlength={0}
|
||||
bind:value={secret}
|
||||
required />
|
||||
<Alert.Inline status="info">
|
||||
To complete the setup, create an OAuth2 client ID with "Web application" as the application
|
||||
type, then add this redirect URI to your {provider.name} configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!secret ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,35 +1,204 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
Button,
|
||||
InputPassword,
|
||||
InputSwitch,
|
||||
InputText,
|
||||
InputTextarea
|
||||
} from '$lib/elements/forms';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { Link, Alert } from '@appwrite.io/pink-svelte';
|
||||
import { OAuthProvider, type Models, type Models as ConsoleModels } from '@appwrite.io/console';
|
||||
import { oAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import {
|
||||
Accordion,
|
||||
Alert,
|
||||
Card,
|
||||
Divider,
|
||||
Icon,
|
||||
Layout,
|
||||
Link,
|
||||
Tag,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconDocument, IconPencil, IconUpload, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
|
||||
const projectId = page.params.project;
|
||||
const region = page.params.region;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
export let parameters: ConsoleModels.ConsoleOAuth2ProviderParameter[] = [];
|
||||
|
||||
let enabled: boolean = null;
|
||||
let appId: string = null;
|
||||
let secret: string = null;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
enabled ??= provider.enabled;
|
||||
appId ??= provider.appId;
|
||||
secret ??= provider.secret;
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
let enabled: boolean = provider.enabled;
|
||||
let appId: string | null = null;
|
||||
let providerKeyId: string | null = null;
|
||||
let fieldValues: Record<string, string | null> = {};
|
||||
let showSecretInput = false;
|
||||
let p8PasteMode: Record<string, boolean> = {};
|
||||
let error: string;
|
||||
let initializedProvider: Models.AuthProvider | null = null;
|
||||
let initialEnabled = false;
|
||||
let initialAppId: string | null = null;
|
||||
let initialDetailValues: Record<string, string> = {};
|
||||
|
||||
$: appIdParam = parameters.length >= 1 ? parameters[0] : null;
|
||||
$: additionalParams = parameters.slice(1);
|
||||
$: detailParams = additionalParams.filter((param) => !isSecretParam(param.$id));
|
||||
$: secretParams = additionalParams.filter((param) => isSecretParam(param.$id));
|
||||
$: basicDetailParams =
|
||||
provider?.key === OAuthProvider.Oidc
|
||||
? detailParams.filter((param) => !isOidcAdvancedParam(param.$id))
|
||||
: detailParams;
|
||||
$: advancedDetailParams =
|
||||
provider?.key === OAuthProvider.Oidc
|
||||
? detailParams.filter((param) => isOidcAdvancedParam(param.$id))
|
||||
: [];
|
||||
$: hasSecretInput = secretParams.some((param) => {
|
||||
const value = fieldValues[param.$id];
|
||||
return value !== null && value !== '';
|
||||
});
|
||||
$: hasDetailChanges = detailParams.some((param) => {
|
||||
return (
|
||||
normalizeFieldValue(fieldValues[param.$id]) !== (initialDetailValues[param.$id] ?? '')
|
||||
);
|
||||
});
|
||||
$: nothingChanged =
|
||||
enabled === initialEnabled &&
|
||||
normalizeFieldValue(appId) === normalizeFieldValue(initialAppId) &&
|
||||
!hasSecretInput &&
|
||||
!hasDetailChanges;
|
||||
$: oAuthProvider = oAuthProviders[provider.key];
|
||||
$: secretCardTitle =
|
||||
secretParams.length === 1 ? primaryName(secretParams[0]?.name ?? '') : 'Credentials';
|
||||
|
||||
$: if (provider && provider !== initializedProvider) {
|
||||
initializedProvider = provider;
|
||||
initialEnabled = provider.enabled;
|
||||
initialAppId = getInitialAppId();
|
||||
providerKeyId = getInitialFieldValue('keyId');
|
||||
initialDetailValues = Object.fromEntries(
|
||||
detailParams.map((param) => [param.$id, getInitialFieldValue(param.$id) ?? ''])
|
||||
);
|
||||
enabled = initialEnabled;
|
||||
appId = initialAppId;
|
||||
fieldValues = Object.fromEntries(
|
||||
additionalParams.map((param) => [param.$id, getInitialFieldValue(param.$id)])
|
||||
);
|
||||
p8PasteMode = {};
|
||||
showSecretInput = !appId || (provider.key === OAuthProvider.Apple && !providerKeyId);
|
||||
error = undefined;
|
||||
}
|
||||
|
||||
function primaryName(name: string): string {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
function helperText(hint?: string | null): string | undefined {
|
||||
const value = hint?.trim();
|
||||
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getInitialFieldValue(id: string): string | null {
|
||||
const value = (provider as Record<string, unknown>)[id];
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
function getInitialAppId(): string | null {
|
||||
const raw = provider as Record<string, unknown>;
|
||||
const value =
|
||||
raw['clientId'] ??
|
||||
raw['appId'] ??
|
||||
raw['oauthClientId'] ??
|
||||
raw['oauth2ClientId'] ??
|
||||
raw['applicationId'] ??
|
||||
raw['serviceId'] ??
|
||||
raw['apiKey'] ??
|
||||
raw['publicKey'] ??
|
||||
raw['appKey'] ??
|
||||
raw['keyString'] ??
|
||||
raw['customerKey'] ??
|
||||
raw['key'];
|
||||
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
function normalizeFieldValue(value: string | null | undefined): string {
|
||||
return value ?? '';
|
||||
}
|
||||
|
||||
function isPasswordParam(id: string): boolean {
|
||||
return id.toLowerCase().includes('secret');
|
||||
}
|
||||
|
||||
function isP8Param(id: string): boolean {
|
||||
return id === 'p8File' || id === 'p8' || id.toLowerCase().includes('p8file');
|
||||
}
|
||||
|
||||
function isSecretParam(id: string): boolean {
|
||||
return isPasswordParam(id) || isP8Param(id);
|
||||
}
|
||||
|
||||
function isOidcAdvancedParam(id: string): boolean {
|
||||
return id === 'authorizationURL' || id === 'tokenUrl' || id === 'userInfoUrl';
|
||||
}
|
||||
|
||||
async function handleP8FileUpload(id: string, event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
fieldValues[id] = await file.text();
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function buildSecret(): string | null {
|
||||
const entries = secretParams.flatMap((param) => {
|
||||
const value = fieldValues[param.$id];
|
||||
|
||||
if (value === null || value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[param.$id, value] as [string, string]];
|
||||
});
|
||||
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
return JSON.stringify(Object.fromEntries(entries));
|
||||
}
|
||||
|
||||
function buildDetails(): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
detailParams.map((param) => [param.$id, fieldValues[param.$id] ?? ''])
|
||||
);
|
||||
}
|
||||
|
||||
function resetWriteOnlyInputs() {
|
||||
showSecretInput = false;
|
||||
|
||||
for (const param of secretParams) {
|
||||
fieldValues[param.$id] = null;
|
||||
}
|
||||
}
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
const result = await updateOAuth({
|
||||
region,
|
||||
projectId,
|
||||
provider,
|
||||
appId,
|
||||
secret: buildSecret(),
|
||||
details: buildDetails(),
|
||||
enabled
|
||||
});
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
@@ -37,6 +206,8 @@
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
// @todo Revisit explicit secret-clearing UX later today. For now, credentials remain update-only.
|
||||
</script>
|
||||
|
||||
<Modal {error} bind:show onSubmit={update} on:close title={`${provider.name} OAuth2 settings`}>
|
||||
@@ -49,21 +220,157 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">visit the docs.</Link.Anchor>
|
||||
</p>
|
||||
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="App ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="App Secret"
|
||||
placeholder="Enter App Secret"
|
||||
minlength={0}
|
||||
bind:value={secret}
|
||||
required />
|
||||
|
||||
{#if appIdParam}
|
||||
<InputText
|
||||
id="appID"
|
||||
label={primaryName(appIdParam.name)}
|
||||
autofocus={true}
|
||||
placeholder={appIdParam.example || ''}
|
||||
helper={helperText(appIdParam.hint)}
|
||||
required={enabled && !!appIdParam.example}
|
||||
bind:value={appId} />
|
||||
{/if}
|
||||
|
||||
{#each basicDetailParams as param}
|
||||
<InputText
|
||||
id={param.$id}
|
||||
label={primaryName(param.name)}
|
||||
placeholder={param.example || ''}
|
||||
helper={helperText(param.hint)}
|
||||
required={enabled && !!param.example}
|
||||
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>
|
||||
<Tag size="s" on:click={() => (showSecretInput = true)}>
|
||||
<Icon icon={IconPencil} size="s" /> Update {secretCardTitle}
|
||||
</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">{secretCardTitle}</Typography.Text>
|
||||
<Button extraCompact on:click={resetWriteOnlyInputs}>
|
||||
<Icon icon={IconX} size="s" />
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
<Typography.Text>
|
||||
{#if secretParams.length === 1}
|
||||
This field is write-only. Enter a new value to update it.
|
||||
{:else}
|
||||
These fields are write-only. Enter new values to update them.
|
||||
{/if}
|
||||
</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>
|
||||
{#each secretParams as param}
|
||||
{#if isP8Param(param.$id)}
|
||||
<div class="p8-field">
|
||||
<div class="p8-field-header">
|
||||
<Typography.Text variant="m-400"
|
||||
>{primaryName(param.name)}</Typography.Text>
|
||||
<div class="p8-field-controls">
|
||||
<Button
|
||||
text
|
||||
extraCompact
|
||||
on:click={() =>
|
||||
(p8PasteMode[param.$id] =
|
||||
!p8PasteMode[param.$id])}>
|
||||
{p8PasteMode[param.$id]
|
||||
? 'Upload file instead'
|
||||
: 'Paste instead'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if p8PasteMode[param.$id]}
|
||||
<InputTextarea
|
||||
id={param.$id}
|
||||
label=""
|
||||
placeholder={'-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'}
|
||||
helper={helperText(param.hint)}
|
||||
bind:value={fieldValues[param.$id]} />
|
||||
{:else}
|
||||
<label
|
||||
class="p8-upload-zone"
|
||||
class:has-file={!!fieldValues[param.$id]}
|
||||
for="p8-file-{param.$id}">
|
||||
{#if fieldValues[param.$id]}
|
||||
<Icon icon={IconDocument} size="s" />
|
||||
<Typography.Text
|
||||
>File loaded — click to replace</Typography.Text>
|
||||
<Button
|
||||
text
|
||||
extraCompact
|
||||
on:click={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
fieldValues[param.$id] = null;
|
||||
}}>
|
||||
<Icon icon={IconX} size="s" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Icon icon={IconUpload} size="m" />
|
||||
<Typography.Text
|
||||
>Click to upload .p8 file</Typography.Text>
|
||||
{/if}
|
||||
</label>
|
||||
<input
|
||||
id="p8-file-{param.$id}"
|
||||
type="file"
|
||||
accept=".p8"
|
||||
style="display:none"
|
||||
on:change={(event) =>
|
||||
handleP8FileUpload(param.$id, event)} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<InputPassword
|
||||
id={param.$id}
|
||||
label={primaryName(param.name)}
|
||||
placeholder={param.example || ''}
|
||||
helper={helperText(param.hint)}
|
||||
minlength={0}
|
||||
bind:value={fieldValues[param.$id]} />
|
||||
{/if}
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
@@ -72,12 +379,52 @@
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!secret ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
<Button disabled={nothingChanged} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.p8-upload-zone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border: 1px dashed var(--border-neutral-secondary);
|
||||
border-radius: var(--border-radius-small);
|
||||
cursor: pointer;
|
||||
background: var(--bgcolor-neutral-secondary);
|
||||
transition: border-color 0.15s;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.p8-upload-zone:hover {
|
||||
border-color: var(--border-neutral-primary);
|
||||
}
|
||||
|
||||
.p8-upload-zone.has-file {
|
||||
border-style: solid;
|
||||
border-color: var(--border-neutral-primary);
|
||||
}
|
||||
|
||||
.p8-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.p8-field-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.p8-field-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let clientSecret: string = null;
|
||||
let tenantID: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) ({ clientSecret, tenantID } = JSON.parse(provider.secret));
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret = clientSecret
|
||||
? JSON.stringify({ clientSecret, ...(tenantID && { tenantID }) })
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} onSubmit={update} bind:show on:close title={`${provider.name} OAuth2 settings`}>
|
||||
<p slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more info
|
||||
you can
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer">
|
||||
visit the docs.
|
||||
</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="Application (client) ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="Client Secret"
|
||||
placeholder="Enter Client Secret"
|
||||
minlength={0}
|
||||
bind:value={clientSecret}
|
||||
required />
|
||||
<InputText
|
||||
id="tenant"
|
||||
label="Target Tenant"
|
||||
placeholder="'common','organizations','consumers' or your TenantID"
|
||||
bind:value={tenantID} />
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!clientSecret ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,133 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let clientSecret: string = null;
|
||||
let wellKnownEndpoint: string = null;
|
||||
let authorizationEndpoint: string = null;
|
||||
let tokenEndpoint: string = null;
|
||||
let userinfoEndpoint: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
// secret is valid if clientSecret is set and either wellKnownEndpoint or all of the other endpoints are set
|
||||
$: isValidSecret =
|
||||
clientSecret &&
|
||||
(wellKnownEndpoint || (authorizationEndpoint && tokenEndpoint && userinfoEndpoint));
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) {
|
||||
({
|
||||
clientSecret,
|
||||
wellKnownEndpoint,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint
|
||||
} = JSON.parse(provider.secret));
|
||||
}
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret = isValidSecret
|
||||
? JSON.stringify({
|
||||
clientSecret,
|
||||
wellKnownEndpoint,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint
|
||||
})
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} onSubmit={update} title="{provider.name} OAuth2 settings" bind:show on:close>
|
||||
<p slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more info
|
||||
you can
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="Client ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="Client Secret"
|
||||
placeholder="Enter Client Secret"
|
||||
minlength={0}
|
||||
bind:value={clientSecret}
|
||||
required />
|
||||
<InputText
|
||||
id="well-known-endpoint"
|
||||
label="Well-Known Endpoint"
|
||||
placeholder="https://example.com/.well-known/openid-configuration"
|
||||
bind:value={wellKnownEndpoint}
|
||||
required={!authorizationEndpoint && !tokenEndpoint && !userinfoEndpoint} />
|
||||
<InputText
|
||||
id="authorization-endpoint"
|
||||
label="Authorization Endpoint"
|
||||
placeholder="https://example.com/authorize"
|
||||
bind:value={authorizationEndpoint}
|
||||
required={!wellKnownEndpoint} />
|
||||
<InputText
|
||||
id="token-endpoint"
|
||||
label="Token Endpoint"
|
||||
placeholder="https://example.com/token"
|
||||
bind:value={tokenEndpoint}
|
||||
required={!wellKnownEndpoint} />
|
||||
<InputText
|
||||
id="userinfo-endpoint"
|
||||
label="User Info Endpoint"
|
||||
placeholder="https://example.com/userinfo"
|
||||
bind:value={userinfoEndpoint}
|
||||
required={!wellKnownEndpoint} />
|
||||
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!clientSecret ||
|
||||
(!wellKnownEndpoint &&
|
||||
(!authorizationEndpoint || !tokenEndpoint || !userinfoEndpoint)) ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,109 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { CopyInput, Modal } from '$lib/components';
|
||||
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from '../updateOAuth';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
export let provider: Models.AuthProvider;
|
||||
export let show = false;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let clientSecret: string = null;
|
||||
let oktaDomain: string = null;
|
||||
let authorizationServerId: string = null;
|
||||
let error: string;
|
||||
let oAuthProvider: Provider;
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) {
|
||||
({ clientSecret, oktaDomain, authorizationServerId } = JSON.parse(provider.secret));
|
||||
}
|
||||
oAuthProvider = oAuthProviders[provider.key];
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret =
|
||||
clientSecret && oktaDomain && authorizationServerId
|
||||
? JSON.stringify({ clientSecret, oktaDomain, authorizationServerId })
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
{error}
|
||||
onSubmit={update}
|
||||
size="m"
|
||||
bind:show
|
||||
on:close
|
||||
title={`${provider.name} OAuth2 settings`}>
|
||||
<p slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more info
|
||||
you can
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="Client ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId}
|
||||
required />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="Client Secret"
|
||||
placeholder="Enter Client Secret"
|
||||
minlength={0}
|
||||
bind:value={clientSecret}
|
||||
required />
|
||||
<InputText
|
||||
id="domain"
|
||||
label="Okta Domain"
|
||||
placeholder="dev-1337.okta.com"
|
||||
bind:value={oktaDomain}
|
||||
required />
|
||||
<InputText
|
||||
id="serverID"
|
||||
label="Authorization Server ID"
|
||||
placeholder="default"
|
||||
bind:value={authorizationServerId}
|
||||
required />
|
||||
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!clientSecret ||
|
||||
!oktaDomain ||
|
||||
!authorizationServerId ||
|
||||
(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -30,6 +30,8 @@
|
||||
let { data }: PageProps = $props();
|
||||
/** Must stay derived from `data` so OAuth/auth toggles reflect `invalidate(Dependencies.PROJECT)` without a full reload. */
|
||||
const project = $derived(data.project);
|
||||
const resolvedOAuthProviders = $derived(data.oauthProviders);
|
||||
const consoleParamsMap = $derived(data.consoleParamsMap);
|
||||
|
||||
let showProvider = $state(false);
|
||||
let selectedProvider: Models.AuthProvider | null = $state(null);
|
||||
@@ -152,7 +154,11 @@
|
||||
Enable the authentication methods you wish to use.
|
||||
<svelte:fragment slot="aside">
|
||||
<Layout.Stack gap="m" class="auth-methods-list">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap="s"
|
||||
class="auth-methods-actions">
|
||||
<Button
|
||||
extraCompact
|
||||
on:click={() => {
|
||||
@@ -172,10 +178,10 @@
|
||||
disabled={shouldDisableDisableAllButton}>Disable all</Button>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="l">
|
||||
<Divider />
|
||||
<Layout.Stack direction="row" wrap="wrap">
|
||||
<Divider class="auth-methods-divider" />
|
||||
<div class="auth-methods-grid">
|
||||
{#each $authMethods.list as box}
|
||||
<span style:flex-basis="30%">
|
||||
<div class="auth-method-item">
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<InputSwitch
|
||||
label={box.label}
|
||||
@@ -190,9 +196,9 @@
|
||||
</span>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
@@ -200,8 +206,8 @@
|
||||
<Layout.Stack>
|
||||
<Typography.Title size="s">OAuth2 Providers</Typography.Title>
|
||||
<ul class="grid-box" style:--grid-gap="1rem" style:--grid-item-size="15rem">
|
||||
{#each project.oAuthProviders
|
||||
.filter((p) => p.name !== 'Mock')
|
||||
{#each resolvedOAuthProviders
|
||||
.filter((p) => p.key !== 'mock' && p.name !== 'Mock')
|
||||
.sort( (a, b) => (a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1) ) as provider}
|
||||
{@const oAuthProvider = oAuthProviders[provider.key]}
|
||||
{#if oAuthProvider && !oAuthProvider.internal}
|
||||
@@ -248,6 +254,7 @@
|
||||
<OAuthProvider
|
||||
bind:provider={selectedProvider}
|
||||
bind:show={showProvider}
|
||||
parameters={consoleParamsMap.get(selectedProvider.key) ?? []}
|
||||
onclose={() => {
|
||||
selectedProvider = null;
|
||||
showProvider = false;
|
||||
@@ -271,3 +278,22 @@
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
|
||||
<style>
|
||||
.auth-methods-grid {
|
||||
display: grid;
|
||||
gap: var(--space-6) var(--space-8);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-method-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-methods-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { oAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models, Models as ConsoleModels } from '@appwrite.io/console';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
/** Extract the appId equivalent from a typed OAuth2 provider model. */
|
||||
function extractAppId(p: Record<string, unknown>): string {
|
||||
return (p['clientId'] ??
|
||||
p['appId'] ??
|
||||
p['oauthClientId'] ??
|
||||
p['oauth2ClientId'] ??
|
||||
p['applicationId'] ??
|
||||
p['serviceId'] ??
|
||||
p['apiKey'] ??
|
||||
p['publicKey'] ??
|
||||
p['appKey'] ??
|
||||
p['keyString'] ??
|
||||
p['customerKey'] ??
|
||||
p['key'] ??
|
||||
'') as string;
|
||||
}
|
||||
|
||||
export const load: PageLoad = async ({ depends, params }) => {
|
||||
depends(Dependencies.PROJECT);
|
||||
|
||||
const [providerList, consoleProviderList] = await Promise.all([
|
||||
sdk.forProject(params.region, params.project).project.listOAuth2Providers(),
|
||||
sdk.forConsole.console.listOAuth2Providers()
|
||||
]);
|
||||
|
||||
const consoleParamsMap = new Map<string, ConsoleModels.ConsoleOAuth2ProviderParameter[]>();
|
||||
for (const cp of consoleProviderList.oAuth2Providers) {
|
||||
consoleParamsMap.set(cp.$id, cp.parameters ?? []);
|
||||
}
|
||||
|
||||
const oauthProviders: Models.AuthProvider[] = providerList.providers
|
||||
.filter((p) => p.$id !== 'mock')
|
||||
.map((p) => {
|
||||
const raw = p as unknown as Record<string, unknown>;
|
||||
return {
|
||||
...raw,
|
||||
key: p.$id,
|
||||
name: oAuthProviders[p.$id]?.name ?? p.$id,
|
||||
appId: extractAppId(raw),
|
||||
secret: '',
|
||||
enabled: p.enabled
|
||||
};
|
||||
});
|
||||
|
||||
return { oauthProviders, consoleParamsMap };
|
||||
};
|
||||
@@ -7,10 +7,12 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import { OAuthProvider, type Models } from '@appwrite.io/console';
|
||||
|
||||
type Args = {
|
||||
region: string;
|
||||
projectId: string;
|
||||
provider: Models.AuthProvider;
|
||||
appId: string;
|
||||
appId: string | null;
|
||||
secret: string;
|
||||
details: Record<string, string>;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -19,11 +21,323 @@ type Return = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export async function updateOAuth({
|
||||
function parseSecret(secret: string) {
|
||||
if (!secret) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(secret);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProjectOAuth({
|
||||
region,
|
||||
projectId,
|
||||
provider,
|
||||
appId,
|
||||
secret,
|
||||
details,
|
||||
enabled
|
||||
}: Args) {
|
||||
const projectSdk = sdk.forProject(region, projectId).project;
|
||||
const parsedSecret = parseSecret(secret);
|
||||
|
||||
const getAppId = (): string => appId ?? '';
|
||||
const getDetail = (key: string): string => details[key] ?? '';
|
||||
const getSecret = (key?: string): string | undefined => {
|
||||
if (key) {
|
||||
const value = parsedSecret[key];
|
||||
return typeof value === 'string' && value !== '' ? value : undefined;
|
||||
}
|
||||
|
||||
const firstValue = Object.values(parsedSecret).find(
|
||||
(value): value is string => typeof value === 'string' && value !== ''
|
||||
);
|
||||
|
||||
return firstValue;
|
||||
};
|
||||
|
||||
switch (provider.key) {
|
||||
case OAuthProvider.Amazon:
|
||||
return projectSdk.updateOAuth2Amazon({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Apple:
|
||||
return projectSdk.updateOAuth2Apple({
|
||||
serviceId: getAppId(),
|
||||
keyId: getDetail('keyId'),
|
||||
teamId: getDetail('teamId'),
|
||||
p8File: getSecret('p8File'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Auth0:
|
||||
return projectSdk.updateOAuth2Auth0({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret('clientSecret'),
|
||||
endpoint: getDetail('endpoint'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Authentik:
|
||||
return projectSdk.updateOAuth2Authentik({
|
||||
endpoint: getDetail('endpoint'),
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret('clientSecret'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Autodesk:
|
||||
return projectSdk.updateOAuth2Autodesk({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Bitbucket:
|
||||
return projectSdk.updateOAuth2Bitbucket({
|
||||
key: getAppId(),
|
||||
secret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Bitly:
|
||||
return projectSdk.updateOAuth2Bitly({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Box:
|
||||
return projectSdk.updateOAuth2Box({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Dailymotion:
|
||||
return projectSdk.updateOAuth2Dailymotion({
|
||||
apiKey: getAppId(),
|
||||
apiSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Discord:
|
||||
return projectSdk.updateOAuth2Discord({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Disqus:
|
||||
return projectSdk.updateOAuth2Disqus({
|
||||
publicKey: getAppId(),
|
||||
secretKey: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Dropbox:
|
||||
return projectSdk.updateOAuth2Dropbox({
|
||||
appKey: getAppId(),
|
||||
appSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Etsy:
|
||||
return projectSdk.updateOAuth2Etsy({
|
||||
keyString: getAppId(),
|
||||
sharedSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Facebook:
|
||||
return projectSdk.updateOAuth2Facebook({
|
||||
appId: getAppId(),
|
||||
appSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Figma:
|
||||
return projectSdk.updateOAuth2Figma({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Fusionauth:
|
||||
return projectSdk.updateOAuth2FusionAuth({
|
||||
endpoint: getDetail('endpoint'),
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret('clientSecret'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Github:
|
||||
return projectSdk.updateOAuth2GitHub({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Gitlab:
|
||||
return projectSdk.updateOAuth2Gitlab({
|
||||
applicationId: getAppId(),
|
||||
secret: getSecret('secret'),
|
||||
endpoint: getDetail('endpoint'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Google:
|
||||
return projectSdk.updateOAuth2Google({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Keycloak:
|
||||
return projectSdk.updateOAuth2Keycloak({
|
||||
endpoint: getDetail('endpoint'),
|
||||
realmName: getDetail('realmName'),
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret('clientSecret'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Kick:
|
||||
return projectSdk.updateOAuth2Kick({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Linkedin:
|
||||
return projectSdk.updateOAuth2Linkedin({
|
||||
clientId: getAppId(),
|
||||
primaryClientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Microsoft:
|
||||
return projectSdk.updateOAuth2Microsoft({
|
||||
tenant: getDetail('tenant'),
|
||||
applicationId: getAppId(),
|
||||
applicationSecret: getSecret('applicationSecret'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Notion:
|
||||
return projectSdk.updateOAuth2Notion({
|
||||
oauthClientId: getAppId(),
|
||||
oauthClientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Oidc:
|
||||
return projectSdk.updateOAuth2Oidc({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret('clientSecret'),
|
||||
wellKnownURL: getDetail('wellKnownURL'),
|
||||
authorizationURL: getDetail('authorizationURL'),
|
||||
tokenUrl: getDetail('tokenUrl'),
|
||||
userInfoUrl: getDetail('userInfoUrl'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Okta:
|
||||
return projectSdk.updateOAuth2Okta({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret('clientSecret'),
|
||||
domain: getDetail('domain'),
|
||||
authorizationServerId: getDetail('authorizationServerId'),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Paypal:
|
||||
return projectSdk.updateOAuth2Paypal({
|
||||
clientId: getAppId(),
|
||||
secretKey: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.PaypalSandbox:
|
||||
return projectSdk.updateOAuth2PaypalSandbox({
|
||||
clientId: getAppId(),
|
||||
secretKey: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Podio:
|
||||
return projectSdk.updateOAuth2Podio({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Salesforce:
|
||||
return projectSdk.updateOAuth2Salesforce({
|
||||
customerKey: getAppId(),
|
||||
customerSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Slack:
|
||||
return projectSdk.updateOAuth2Slack({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Spotify:
|
||||
return projectSdk.updateOAuth2Spotify({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Stripe:
|
||||
return projectSdk.updateOAuth2Stripe({
|
||||
clientId: getAppId(),
|
||||
apiSecretKey: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Tradeshift:
|
||||
return projectSdk.updateOAuth2Tradeshift({
|
||||
oauth2ClientId: getAppId(),
|
||||
oauth2ClientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.TradeshiftBox:
|
||||
return projectSdk.updateOAuth2TradeshiftSandbox({
|
||||
oauth2ClientId: getAppId(),
|
||||
oauth2ClientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Twitch:
|
||||
return projectSdk.updateOAuth2Twitch({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Wordpress:
|
||||
return projectSdk.updateOAuth2WordPress({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.X:
|
||||
return projectSdk.updateOAuth2X({
|
||||
customerKey: getAppId(),
|
||||
secretKey: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Yahoo:
|
||||
return projectSdk.updateOAuth2Yahoo({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Yandex:
|
||||
return projectSdk.updateOAuth2Yandex({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Zoho:
|
||||
return projectSdk.updateOAuth2Zoho({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
case OAuthProvider.Zoom:
|
||||
return projectSdk.updateOAuth2Zoom({
|
||||
clientId: getAppId(),
|
||||
clientSecret: getSecret(),
|
||||
enabled
|
||||
});
|
||||
default:
|
||||
throw new Error(`Unsupported OAuth2 provider: ${provider.key}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOAuth({
|
||||
region,
|
||||
projectId,
|
||||
provider,
|
||||
appId,
|
||||
secret,
|
||||
details,
|
||||
enabled
|
||||
}: Args): Promise<Return> {
|
||||
try {
|
||||
@@ -31,13 +345,7 @@ export async function updateOAuth({
|
||||
throw new Error(`Invalid OAuth2 provider: ${provider.key}`);
|
||||
}
|
||||
|
||||
await sdk.forConsole.projects.updateOAuth2({
|
||||
projectId,
|
||||
provider: provider.key,
|
||||
appId: appId || undefined,
|
||||
secret: secret || undefined,
|
||||
enabled
|
||||
});
|
||||
await updateProjectOAuth({ region, projectId, provider, appId, secret, details, enabled });
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
|
||||
addNotification({
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default as Save } from './save.svelte';
|
||||
export { default as Error } from './error.svelte';
|
||||
export { default as Error } from './errorSonner.svelte';
|
||||
export { default as Suggestions } from './suggestions.svelte';
|
||||
|
||||
@@ -90,7 +90,10 @@
|
||||
scopes: ($func.scopes as Scopes[]) || undefined,
|
||||
installationId: $installation.$id || undefined,
|
||||
providerRepositoryId: selectedRepository || undefined,
|
||||
providerBranch: branch || undefined
|
||||
providerBranch: branch || undefined,
|
||||
providerSilentMode: $func.providerSilentMode ?? undefined,
|
||||
providerRootDirectory: $func.providerRootDirectory ?? undefined,
|
||||
buildSpecification: $func.buildSpecification || undefined
|
||||
});
|
||||
}
|
||||
if (commit) {
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
providerRepositoryId: '',
|
||||
providerBranch: '',
|
||||
providerSilentMode: true,
|
||||
providerRootDirectory: ''
|
||||
providerRootDirectory: '',
|
||||
buildSpecification: $func.buildSpecification || undefined
|
||||
});
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
dispatch('success');
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
providerRepositoryId: func.providerRepositoryId || undefined,
|
||||
providerBranch: selectedBranch,
|
||||
providerSilentMode: silentMode,
|
||||
providerRootDirectory: selectedDir
|
||||
providerRootDirectory: selectedDir,
|
||||
buildSpecification: func.buildSpecification || undefined
|
||||
});
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
@@ -129,7 +130,21 @@
|
||||
selectedDir !== func?.providerRootDirectory;
|
||||
|
||||
async function connect(selectedInstallationId: string, selectedRepository: string) {
|
||||
let nextBranch = func?.providerBranch ?? 'main';
|
||||
try {
|
||||
const branchList = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.vcs.listRepositoryBranches({
|
||||
installationId: selectedInstallationId,
|
||||
providerRepositoryId: selectedRepository
|
||||
});
|
||||
const sorted = sortBranches(branchList.branches);
|
||||
nextBranch =
|
||||
sorted.find((branch) => branch.name === func?.providerBranch)?.name ??
|
||||
sorted.find((branch) => branch.name === 'main' || branch.name === 'master')?.name ??
|
||||
sorted[0]?.name ??
|
||||
nextBranch;
|
||||
|
||||
if (!isValueOfStringEnum(Runtime, func.runtime)) {
|
||||
throw new Error(`Invalid runtime: ${func.runtime}`);
|
||||
}
|
||||
@@ -148,7 +163,10 @@
|
||||
scopes: (func.scopes as Scopes[]) || undefined,
|
||||
installationId: selectedInstallationId,
|
||||
providerRepositoryId: selectedRepository,
|
||||
providerBranch: 'main'
|
||||
providerBranch: nextBranch,
|
||||
providerSilentMode: func.providerSilentMode ?? undefined,
|
||||
providerRootDirectory: func.providerRootDirectory ?? undefined,
|
||||
buildSpecification: func.buildSpecification || undefined
|
||||
});
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
} catch {
|
||||
@@ -275,7 +293,7 @@
|
||||
{#if showSelectRoot}
|
||||
<SelectRootModal
|
||||
bind:show={showSelectRoot}
|
||||
product="sites"
|
||||
product="functions"
|
||||
bind:rootDir={selectedDir}
|
||||
branch={selectedBranch} />
|
||||
{/if}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
const compatPairs = [
|
||||
{ newer: 'tables.', legacy: 'collections.' },
|
||||
{ newer: 'columns.', legacy: 'attributes.' },
|
||||
{ newer: 'rows.', legacy: 'documents.' }
|
||||
{ newer: 'rows.', legacy: 'documents.' },
|
||||
{ newer: 'executions.', legacy: 'execution.' }
|
||||
] as const;
|
||||
|
||||
export function getEffectiveScopes(scopes: string[]): string[] {
|
||||
@@ -30,64 +31,134 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { scopes as allScopes, cloudOnlyBackupScopes } from '$lib/constants';
|
||||
import { Accordion, Badge, Divider, Layout, Selector } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
cloudOnlyBackupScopes,
|
||||
scopes as localScopes,
|
||||
type ScopeDefinition
|
||||
} from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Accordion, Alert, Badge, Divider, Layout, Selector } from '@appwrite.io/pink-svelte';
|
||||
import type { Scopes } from '@appwrite.io/console';
|
||||
|
||||
export let scopes: Scopes[];
|
||||
let { scopes = $bindable([]) }: { scopes: Scopes[] } = $props();
|
||||
|
||||
// insert cloud-only scopes right after databases.write
|
||||
const databasesWriteIndex = allScopes.findIndex((s) => s.scope === 'databases.write');
|
||||
const filteredScopes =
|
||||
isCloud && databasesWriteIndex !== -1
|
||||
? [
|
||||
...allScopes.slice(0, databasesWriteIndex + 1),
|
||||
...cloudOnlyBackupScopes,
|
||||
...allScopes.slice(databasesWriteIndex + 1)
|
||||
]
|
||||
: allScopes;
|
||||
let allScopesList: ScopeDefinition[] = $state([]);
|
||||
let mounted = $state(false);
|
||||
let loadError: string | null = $state(null);
|
||||
const effectiveScopes = $derived(getEffectiveScopes(scopes));
|
||||
|
||||
// include all scopes
|
||||
const scopeCatalog = new Set([
|
||||
...allScopes.map((s) => s.scope),
|
||||
...(isCloud ? cloudOnlyBackupScopes.map((s) => s.scope) : [])
|
||||
]);
|
||||
const categoryAliasMap: Record<string, string> = {
|
||||
Database: 'Databases'
|
||||
};
|
||||
|
||||
enum Category {
|
||||
Project = 'Project',
|
||||
Auth = 'Auth',
|
||||
Database = 'Database',
|
||||
Databases = 'Databases',
|
||||
Functions = 'Functions',
|
||||
Messaging = 'Messaging',
|
||||
Sites = 'Sites',
|
||||
Storage = 'Storage',
|
||||
Domains = 'Domains',
|
||||
Other = 'Other'
|
||||
}
|
||||
|
||||
const categories = [
|
||||
const categoryOrder = [
|
||||
Category.Project,
|
||||
Category.Auth,
|
||||
Category.Database,
|
||||
Category.Databases,
|
||||
Category.Functions,
|
||||
Category.Storage,
|
||||
Category.Messaging,
|
||||
Category.Sites,
|
||||
Category.Domains,
|
||||
Category.Other
|
||||
];
|
||||
|
||||
let mounted = false;
|
||||
function normalizeCategory(category: string): string {
|
||||
return categoryAliasMap[category] ?? category;
|
||||
}
|
||||
|
||||
const activeScopes = filteredScopes.reduce((prev, next) => {
|
||||
prev[next.scope] = false;
|
||||
return prev;
|
||||
}, {});
|
||||
const filteredScopes = $derived.by(() => {
|
||||
const databasesWriteIndex = allScopesList.findIndex((s) => s.scope === 'databases.write');
|
||||
if (isCloud && databasesWriteIndex !== -1) {
|
||||
return [
|
||||
...allScopesList.slice(0, databasesWriteIndex + 1),
|
||||
...cloudOnlyBackupScopes.map((scope) => ({
|
||||
...scope,
|
||||
category: normalizeCategory(scope.category)
|
||||
})),
|
||||
...allScopesList.slice(databasesWriteIndex + 1)
|
||||
];
|
||||
}
|
||||
return allScopesList;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
scopes.forEach((scope) => {
|
||||
if (scope in activeScopes) {
|
||||
activeScopes[scope] = true;
|
||||
const categories = $derived.by(() => {
|
||||
const availableCategories = new Set(
|
||||
filteredScopes.map((scope) => normalizeCategory(scope.category))
|
||||
);
|
||||
|
||||
return [
|
||||
...categoryOrder.filter((category) => availableCategories.has(category)),
|
||||
...Array.from(availableCategories).filter(
|
||||
(category) => !categoryOrder.includes(category as Category)
|
||||
)
|
||||
];
|
||||
});
|
||||
|
||||
const scopeCatalog = $derived(
|
||||
new Set([
|
||||
...filteredScopes.map((s) => s.scope),
|
||||
...(isCloud ? cloudOnlyBackupScopes.map((s) => s.scope) : [])
|
||||
])
|
||||
);
|
||||
|
||||
let activeScopes: Record<string, boolean> = $state({});
|
||||
|
||||
$effect(() => {
|
||||
if (mounted) {
|
||||
const newScopes = generateSyncedScopes(activeScopes);
|
||||
if (symmetricDifference(scopes, newScopes).length) {
|
||||
scopes = newScopes;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mounted = true;
|
||||
onMount(async () => {
|
||||
try {
|
||||
const result = await sdk.forConsole.console.listProjectScopes();
|
||||
const scopesById = new Map<string, ScopeDefinition>();
|
||||
|
||||
for (const scope of localScopes) {
|
||||
scopesById.set(scope.scope, {
|
||||
...scope,
|
||||
category: normalizeCategory(scope.category)
|
||||
});
|
||||
}
|
||||
|
||||
for (const scope of result.scopes) {
|
||||
scopesById.set(scope.$id, {
|
||||
scope: scope.$id,
|
||||
description: scope.description,
|
||||
category: normalizeCategory(scope.category),
|
||||
deprecated: scope.deprecated,
|
||||
icon: ''
|
||||
});
|
||||
}
|
||||
|
||||
allScopesList = Array.from(scopesById.values());
|
||||
|
||||
for (const s of filteredScopes) {
|
||||
activeScopes[s.scope] = effectiveScopes.includes(s.scope);
|
||||
}
|
||||
mounted = true;
|
||||
} catch (e) {
|
||||
loadError = e?.message ?? 'Failed to load available scopes.';
|
||||
// mounted intentionally stays false — the $effect guards on it,
|
||||
// so leaving it false prevents activeScopes = {} from overwriting
|
||||
// the parent-bound scopes prop with an empty array on fetch failure.
|
||||
}
|
||||
});
|
||||
|
||||
function selectAll() {
|
||||
@@ -104,8 +175,9 @@
|
||||
|
||||
function categoryState(category: string, s: string[]): boolean | 'indeterminate' {
|
||||
const scopesByCategory = filteredScopes.filter((n) => n.category === category);
|
||||
const scopeSet = new Set(getEffectiveScopes(s));
|
||||
const activeInCategory = scopesByCategory.filter((scopeItem) =>
|
||||
s.includes(scopeItem.scope as Scopes)
|
||||
scopeSet.has(scopeItem.scope)
|
||||
);
|
||||
|
||||
if (activeInCategory.length === 0) {
|
||||
@@ -117,11 +189,12 @@
|
||||
return 'indeterminate';
|
||||
}
|
||||
|
||||
function onCategoryChange(event: CustomEvent<boolean | 'indeterminate'>, category: Category) {
|
||||
if (event.detail === 'indeterminate') return;
|
||||
function onCategoryChange(event: CustomEvent<boolean | 'indeterminate'>, category: string) {
|
||||
const { detail } = event;
|
||||
if (detail === 'indeterminate') return;
|
||||
filteredScopes.forEach((s) => {
|
||||
if (s.category === category && !s.deprecated) {
|
||||
activeScopes[s.scope] = event.detail;
|
||||
activeScopes[s.scope] = detail;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -131,19 +204,12 @@
|
||||
.filter(([scope, isActive]) => isActive && scopeCatalog.has(scope))
|
||||
.map(([scope]) => scope as Scopes);
|
||||
}
|
||||
|
||||
$: {
|
||||
if (mounted) {
|
||||
const newScopes = generateSyncedScopes(activeScopes);
|
||||
|
||||
if (symmetricDifference(scopes, newScopes).length) {
|
||||
scopes = newScopes;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout.Stack>
|
||||
{#if loadError}
|
||||
<Alert.Inline status="error" title="Failed to load scopes">{loadError}</Alert.Inline>
|
||||
{/if}
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<Button compact on:click={selectAll}>Select all</Button>
|
||||
<span style:height="20px">
|
||||
@@ -158,7 +224,7 @@
|
||||
{@const checked = categoryState(category, scopes)}
|
||||
{@const isLastItem = index === categories.length - 1}
|
||||
{@const scopesLength = filteredScopes.filter(
|
||||
(n) => n.category === category && scopes.includes(n.scope as Scopes)
|
||||
(n) => n.category === category && effectiveScopes.includes(n.scope)
|
||||
).length}
|
||||
<Accordion
|
||||
selectable
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
// send the project to correct region pool for deletion!
|
||||
await sdk.forConsoleIn($project.region).projects.delete({
|
||||
projectId: $project.$id
|
||||
});
|
||||
await sdk.forProject($project.region, $project.$id).project.delete();
|
||||
await finishAndRedirect();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
Scopes.FilesRead,
|
||||
Scopes.BucketsRead,
|
||||
Scopes.FunctionsRead,
|
||||
Scopes.ExecutionRead,
|
||||
Scopes.ExecutionsRead,
|
||||
Scopes.LocaleRead,
|
||||
Scopes.AvatarsRead,
|
||||
Scopes.HealthRead
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
Scopes.FilesRead,
|
||||
Scopes.BucketsRead,
|
||||
Scopes.FunctionsRead,
|
||||
Scopes.ExecutionRead,
|
||||
Scopes.ExecutionsRead,
|
||||
Scopes.SitesRead,
|
||||
Scopes.ProvidersRead,
|
||||
Scopes.TopicsRead,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0a1.667 1.667 0 0 0-1.666 1.666A1.667 1.667 0 0 0 12 3.334a1.667 1.667 0 0 0 1.666-1.668A1.667 1.667 0 0 0 12 0ZM9.506 1.145A11.572 11.572 0 0 0 .442 12.317a.716.716 0 0 0 1.075.618.702.702 0 0 0 .358-.6 10.136 10.136 0 0 1 5.06-8.666c.843-.486 1.75-.848 2.696-1.074a2.55 2.55 0 0 1-.125-1.452Zm8.015 1.26a.712.712 0 0 0-.695.713.704.704 0 0 0 .34.61 10.133 10.133 0 0 1 4.545 11.587c.314.046.618.15.894.309.148.084.29.183.42.293a11.573 11.573 0 0 0-5.15-13.43.715.715 0 0 0-.354-.082Zm-5.519 3.791a6.247 6.247 0 1 0 .002 12.494 6.247 6.247 0 0 0-.002-12.494Zm0 1.43a4.819 4.819 0 0 1 3.41 8.222 4.817 4.817 0 1 1-3.41-8.222Zm-.01 2.295c-1.412.014-1.896 1.887-.668 2.584l-.435 2.207a.237.237 0 0 0 .234.281h1.772a.236.236 0 0 0 .234-.281l-.438-2.207c.43-.247.694-.706.692-1.202a1.38 1.38 0 0 0-1.39-1.382zm-9.324 6.242a1.667 1.667 0 0 0-1.666 1.666 1.667 1.667 0 0 0 1.666 1.668 1.667 1.667 0 0 0 1.666-1.668 1.667 1.667 0 0 0-1.666-1.666zm18.664 0a1.667 1.667 0 0 0-1.666 1.666 1.667 1.667 0 0 0 1.666 1.668 1.667 1.667 0 0 0 1.666-1.668 1.667 1.667 0 0 0-1.666-1.666zM4.655 19.427c-.195.244-.432.45-.702.608a2.584 2.584 0 0 1-.468.207 11.576 11.576 0 0 0 14.208 2.273.713.713 0 0 0 0-1.238.704.704 0 0 0-.703-.012 10.134 10.134 0 0 1-10.052-.05 10.17 10.17 0 0 1-2.283-1.788z" fill="#F58320"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18.742 1.182-12.493.002C4.155 4.784 2.079 8.393 0 12.002c2.071 3.612 4.162 7.214 6.252 10.816l12.49-.004 3.089-5.404h2.158v-.002H24L23.996 6.59h-2.168zM8.327 4.792h2.081l1.04 1.8-3.12 5.413 3.117 5.403-1.035 1.81H8.327a2047.566 2047.566 0 0 0-4.168-7.204C5.547 9.606 6.937 7.2 8.327 4.792Zm6.241 0 2.086.003c1.393 2.405 2.78 4.813 4.166 7.222l-4.167 7.2h-2.08c-.382-.562-1.038-1.808-1.038-1.808l3.123-5.405-3.124-5.413z" fill="#E6E6E6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 551 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z" fill="#53FC19"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 3H18.75C19.9926 3 21 4.00736 21 5.25V18.75C21 19.9926 19.9926 21 18.75 21H5.25C4.00736 21 3 19.9926 3 18.75V5.25C3 4.00736 4.00736 3 5.25 3ZM13.5467 9.2378C14.8834 9.40936 16.2798 9.84718 17.6601 10.6272L19.0312 9.71202V12.3053H15.0701L16.2892 11.5423C15.3212 11.1022 14.4026 10.8227 13.5467 10.6742V17.0335L11.4138 18.4063C1.04033 16.518 4.80411 9.69874 11.4138 9.18233V6.96653L13.5467 5.59371V9.2378ZM11.4137 10.5686V17.0335C4.98567 16.1994 6.46334 10.9884 11.4137 10.5686Z" fill="#F68423"/>
|
||||
<svg width="76" height="76" viewBox="0 0 76 76" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M46.605 67.659L34.519 73.325V8.665L46.605 3V67.659Z" fill="#F7931E"/>
|
||||
<path d="M11.479 48.5477C11.479 40.3947 20.467 33.5297 32.706 31.4477V24.2367C14.069 26.4557 0 36.4947 0 48.5477C0 60.9907 14.99 71.2897 34.519 73.0557V65.9217C21.362 64.2387 11.479 57.1007 11.479 48.5477Z" fill="#B3B3B3"/>
|
||||
<path d="M75.915 43.9432L74.328 27.3982L68.631 30.7472C63.152 27.5042 56.189 25.1952 48.418 24.2522V31.4752C52.943 32.2592 57.011 33.7022 60.343 35.6222L54.159 39.2572L75.915 43.9432Z" fill="#B3B3B3"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 608 B |
@@ -1,26 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.2582 12.2903C19.2582 15.7276 17.6529 18.825 15.0848 21C5.90332 12.2903 5.90332 12.2903 5.90332 12.2903C14.25 12.2903 16.3367 12.2904 19.2582 12.2903Z" fill="url(#paint0_linear_2689_27578)"/>
|
||||
<path d="M19.2582 12.2903C15.7107 12.2903 12.7893 12.2903 5.90332 12.2903C5.90332 12.2903 5.90332 12.2903 15.0848 3C17.6529 5.32001 19.2582 8.62389 19.2582 12.2903Z" fill="url(#paint1_linear_2689_27578)"/>
|
||||
<path d="M20.9033 16.3548C21.3427 14.9866 21.5807 13.5223 21.5807 12C21.5807 10.4778 21.3427 9.01342 20.9033 7.64517L5.90332 12.2903L20.9033 16.3548Z" fill="url(#paint2_linear_2689_27578)"/>
|
||||
<path d="M7.06445 10.6452C7.06445 8.98832 8.4076 7.64517 10.0645 7.64517H11.6128C13.2697 7.64517 14.6128 8.98832 14.6128 10.6452V16.2492C14.6128 17.2696 13.7856 18.0968 12.7652 18.0968V18.0968C12.2752 18.0968 11.8053 17.9021 11.4588 17.5556L10.8386 16.9355H10.0645C8.4076 16.9355 7.06445 15.5923 7.06445 13.9355V10.6452Z" fill="black" fill-opacity="0.3"/>
|
||||
<rect x="3" y="6.48386" width="10.4516" height="10.4516" rx="2" fill="url(#paint3_linear_2689_27578)"/>
|
||||
<path d="M8.09601 11.1242L6.72715 8.80646H5.32251L7.55025 12.5473V14.6129H8.89227V12.5473L11.129 8.80646H9.71537L8.34652 11.1242H8.09601Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2689_27578" x1="11.5373" y1="15.1195" x2="17.2972" y2="18.625" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0E51A3"/>
|
||||
<stop offset="1" stop-color="#1464B6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2689_27578" x1="10.494" y1="10.062" x2="17.4732" y2="6.02856" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2292E2"/>
|
||||
<stop offset="1" stop-color="#2294DB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2689_27578" x1="10.293" y1="12.2082" x2="21.3717" y2="12.2082" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#176FE0"/>
|
||||
<stop offset="1" stop-color="#2F9FEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_2689_27578" x1="3" y1="11.7097" x2="13.4516" y2="11.7097" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#084C8A"/>
|
||||
<stop offset="1" stop-color="#1572CB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0a1.667 1.667 0 0 0-1.666 1.666A1.667 1.667 0 0 0 12 3.334a1.667 1.667 0 0 0 1.666-1.668A1.667 1.667 0 0 0 12 0ZM9.506 1.145A11.572 11.572 0 0 0 .442 12.317a.716.716 0 0 0 1.075.618.702.702 0 0 0 .358-.6 10.136 10.136 0 0 1 5.06-8.666c.843-.486 1.75-.848 2.696-1.074a2.55 2.55 0 0 1-.125-1.452Zm8.015 1.26a.712.712 0 0 0-.695.713.704.704 0 0 0 .34.61 10.133 10.133 0 0 1 4.545 11.587c.314.046.618.15.894.309.148.084.29.183.42.293a11.573 11.573 0 0 0-5.15-13.43.715.715 0 0 0-.354-.082Zm-5.519 3.791a6.247 6.247 0 1 0 .002 12.494 6.247 6.247 0 0 0-.002-12.494Zm0 1.43a4.819 4.819 0 0 1 3.41 8.222 4.817 4.817 0 1 1-3.41-8.222Zm-.01 2.295c-1.412.014-1.896 1.887-.668 2.584l-.435 2.207a.237.237 0 0 0 .234.281h1.772a.236.236 0 0 0 .234-.281l-.438-2.207c.43-.247.694-.706.692-1.202a1.38 1.38 0 0 0-1.39-1.382zm-9.324 6.242a1.667 1.667 0 0 0-1.666 1.666 1.667 1.667 0 0 0 1.666 1.668 1.667 1.667 0 0 0 1.666-1.668 1.667 1.667 0 0 0-1.666-1.666zm18.664 0a1.667 1.667 0 0 0-1.666 1.666 1.667 1.667 0 0 0 1.666 1.668 1.667 1.667 0 0 0 1.666-1.668 1.667 1.667 0 0 0-1.666-1.666zM4.655 19.427c-.195.244-.432.45-.702.608a2.584 2.584 0 0 1-.468.207 11.576 11.576 0 0 0 14.208 2.273.713.713 0 0 0 0-1.238.704.704 0 0 0-.703-.012 10.134 10.134 0 0 1-10.052-.05 10.17 10.17 0 0 1-2.283-1.788z" fill="#F58320"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18.742 1.182-12.493.002C4.155 4.784 2.079 8.393 0 12.002c2.071 3.612 4.162 7.214 6.252 10.816l12.49-.004 3.089-5.404h2.158v-.002H24L23.996 6.59h-2.168zM8.327 4.792h2.081l1.04 1.8-3.12 5.413 3.117 5.403-1.035 1.81H8.327a2047.566 2047.566 0 0 0-4.168-7.204C5.547 9.606 6.937 7.2 8.327 4.792Zm6.241 0 2.086.003c1.393 2.405 2.78 4.813 4.166 7.222l-4.167 7.2h-2.08c-.382-.562-1.038-1.808-1.038-1.808l3.123-5.405-3.124-5.413z" fill="#4D4D4D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 551 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z" fill="#53FC19"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 3H18.75C19.9926 3 21 4.00736 21 5.25V18.75C21 19.9926 19.9926 21 18.75 21H5.25C4.00736 21 3 19.9926 3 18.75V5.25C3 4.00736 4.00736 3 5.25 3ZM13.5467 9.2378C14.8834 9.40936 16.2798 9.84718 17.6601 10.6272L19.0312 9.71202V12.3053H15.0701L16.2892 11.5423C15.3212 11.1022 14.4026 10.8227 13.5467 10.6742V17.0335L11.4138 18.4063C1.04033 16.518 4.80411 9.69874 11.4138 9.18233V6.96653L13.5467 5.59371V9.2378ZM11.4137 10.5686V17.0335C4.98567 16.1994 6.46334 10.9884 11.4137 10.5686Z" fill="#F68423"/>
|
||||
<svg width="76" height="76" viewBox="0 0 76 76" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M46.605 67.659L34.519 73.325V8.665L46.605 3V67.659Z" fill="#F7931E"/>
|
||||
<path d="M11.479 48.5477C11.479 40.3947 20.467 33.5297 32.706 31.4477V24.2367C14.069 26.4557 0 36.4947 0 48.5477C0 60.9907 14.99 71.2897 34.519 73.0557V65.9217C21.362 64.2387 11.479 57.1007 11.479 48.5477Z" fill="#B3B3B3"/>
|
||||
<path d="M75.915 43.9432L74.328 27.3982L68.631 30.7472C63.152 27.5042 56.189 25.1952 48.418 24.2522V31.4752C52.943 32.2592 57.011 33.7022 60.343 35.6222L54.159 39.2572L75.915 43.9432Z" fill="#B3B3B3"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 608 B |
@@ -1,26 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.2582 12.2903C19.2582 15.7276 17.6529 18.825 15.0848 21C5.90332 12.2903 5.90332 12.2903 5.90332 12.2903C14.25 12.2903 16.3367 12.2903 19.2582 12.2903Z" fill="url(#paint0_linear_516_10656)"/>
|
||||
<path d="M19.2582 12.2903C15.7107 12.2903 12.7893 12.2903 5.90332 12.2903C5.90332 12.2903 5.90332 12.2903 15.0848 3C17.6529 5.32001 19.2582 8.62389 19.2582 12.2903Z" fill="url(#paint1_linear_516_10656)"/>
|
||||
<path d="M20.9033 16.3548C21.3427 14.9866 21.5807 13.5222 21.5807 12C21.5807 10.4777 21.3427 9.01339 20.9033 7.64514L5.90332 12.2903L20.9033 16.3548Z" fill="url(#paint2_linear_516_10656)"/>
|
||||
<path d="M7.06445 10.6451C7.06445 8.98829 8.4076 7.64514 10.0645 7.64514H11.6128C13.2697 7.64514 14.6128 8.98829 14.6128 10.6451V16.2492C14.6128 17.2696 13.7856 18.0968 12.7652 18.0968C12.2752 18.0968 11.8053 17.9021 11.4588 17.5556L10.8386 16.9355H10.0645C8.4076 16.9355 7.06445 15.5923 7.06445 13.9355V10.6451Z" fill="black" fill-opacity="0.3"/>
|
||||
<rect x="3" y="6.48389" width="10.4516" height="10.4516" rx="2" fill="url(#paint3_linear_516_10656)"/>
|
||||
<path d="M8.09601 11.1241L6.72715 8.8064H5.32251L7.55025 12.5472V14.6128H8.89227V12.5472L11.129 8.8064H9.71537L8.34652 11.1241H8.09601Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_516_10656" x1="11.5373" y1="15.1194" x2="17.2972" y2="18.625" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0E51A3"/>
|
||||
<stop offset="1" stop-color="#1464B6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_516_10656" x1="10.494" y1="10.062" x2="17.4732" y2="6.02856" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2292E2"/>
|
||||
<stop offset="1" stop-color="#2294DB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_516_10656" x1="10.293" y1="12.2082" x2="21.3717" y2="12.2082" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#176FE0"/>
|
||||
<stop offset="1" stop-color="#2F9FEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_516_10656" x1="3" y1="11.7097" x2="13.4516" y2="11.7097" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#084C8A"/>
|
||||
<stop offset="1" stop-color="#1572CB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |