From 5488f4734e49fd163a316f496ba1eb14846c1c31 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 19:03:06 +0530 Subject: [PATCH 01/27] feat: update console OAuth2 provider support --- bun.lock | 4 +- package.json | 2 +- src/lib/stores/oauth-providers.ts | 20 ++ .../auth/(providers)/appleOAuth.svelte | 3 +- .../auth/(providers)/auth0OAuth.svelte | 3 +- .../auth/(providers)/authentikOAuth.svelte | 3 +- .../auth/(providers)/fusionauthOAuth.svelte | 97 ++++++ .../auth/(providers)/gitlabOAuth.svelte | 3 +- .../auth/(providers)/googleOAuth.svelte | 3 +- .../auth/(providers)/keycloakOAuth.svelte | 108 +++++++ .../auth/(providers)/mainOAuth.svelte | 3 +- .../auth/(providers)/microsoftOAuth.svelte | 3 +- .../auth/(providers)/oidcOAuth.svelte | 3 +- .../auth/(providers)/oktaOAuth.svelte | 3 +- .../auth/settings/+page.svelte | 36 ++- .../auth/updateOAuth.ts | 304 +++++++++++++++++- static/icons/dark/color/fusionauth.svg | 3 + static/icons/dark/color/keycloak.svg | 3 + static/icons/dark/color/kick.svg | 3 + static/icons/dark/color/oidc.svg | 6 +- static/icons/light/color/fusionauth.svg | 3 + static/icons/light/color/keycloak.svg | 3 + static/icons/light/color/kick.svg | 3 + static/icons/light/color/oidc.svg | 6 +- 24 files changed, 599 insertions(+), 29 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte create mode 100644 static/icons/dark/color/fusionauth.svg create mode 100644 static/icons/dark/color/keycloak.svg create mode 100644 static/icons/dark/color/kick.svg create mode 100644 static/icons/light/color/fusionauth.svg create mode 100644 static/icons/light/color/keycloak.svg create mode 100644 static/icons/light/color/kick.svg diff --git a/bun.lock b/bun.lock index 10ce13d78..dacb63cc5 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@88c189e", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d10b661", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", @@ -124,7 +124,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@88c189e", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@d10b661", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index a00683318..7efc7474e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@88c189e", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d10b661", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index 11191dbaf..cffd6fd41 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -2,8 +2,10 @@ 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 FusionAuth from '$routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.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 Keycloak from '$routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.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'; @@ -108,6 +110,12 @@ export const oAuthProviders: Record = { 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: FusionAuth + }, github: { name: 'GitHub', icon: 'github', @@ -140,6 +148,18 @@ export const oAuthProviders: Record = { component: Google, internal: true }, + keycloak: { + name: 'Keycloak', + icon: 'keycloak', + docs: 'https://www.keycloak.org/securing-apps/oidc-layers', + component: Keycloak + }, + kick: { + name: 'Kick', + icon: 'kick', + docs: 'https://docs.kick.com/getting-started/using-the-api', + component: Main + }, linkedin: { name: 'LinkedIn', icon: 'linkedin', diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte index 0554e9598..34aa8c847 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte @@ -10,6 +10,7 @@ import { Link, Alert } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; + const region = page.params.region; export let provider: Models.AuthProvider; export let show = false; @@ -30,7 +31,7 @@ }); const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte index 9b114e740..fb65dadd4 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte @@ -10,6 +10,7 @@ import { Link, Alert } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; + const region = page.params.region; export let provider: Models.AuthProvider; export let show = false; @@ -28,7 +29,7 @@ }); const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte index 19a3b40c6..0b65d7c3b 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte @@ -10,6 +10,7 @@ import { Link, Alert } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; + const region = page.params.region; export let provider: Models.AuthProvider; export let show = false; @@ -29,7 +30,7 @@ }); const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte new file mode 100644 index 000000000..f74a0f593 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte @@ -0,0 +1,97 @@ + + + +

+ To use {provider.name} authentication in your application, first fill in this form. For more info + you can + visit the docs. +

+ + + + + + To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. + + + + + + +
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte index fc46679bd..025f2419e 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte @@ -10,6 +10,7 @@ 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; @@ -29,7 +30,7 @@ }); const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte index d5457b28b..05e5560f2 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte @@ -10,6 +10,7 @@ import { Alert } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; + const region = page.params.region; export let provider: Models.AuthProvider; export let show = false; @@ -29,7 +30,7 @@ let error: string; const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte new file mode 100644 index 000000000..217838736 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte @@ -0,0 +1,108 @@ + + + +

+ To use {provider.name} authentication in your application, first fill in this form. For more info + you can + visit the docs. +

+ + + + + + + To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. + + + + + + +
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index 365782e6b..44109091b 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -10,6 +10,7 @@ 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; @@ -29,7 +30,7 @@ let error: string; const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte index e6679ab2e..d192a3b97 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte @@ -10,6 +10,7 @@ import { Alert } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; + const region = page.params.region; export let provider: Models.AuthProvider; export let show = false; @@ -29,7 +30,7 @@ }); const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte index 1360c4b72..377ddefa1 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte @@ -10,6 +10,7 @@ import { Alert } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; + const region = page.params.region; export let provider: Models.AuthProvider; export let show = false; @@ -45,7 +46,7 @@ }); const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte index 57f5c9e2c..069211370 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte @@ -10,6 +10,7 @@ import { Alert } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; + const region = page.params.region; export let provider: Models.AuthProvider; export let show = false; @@ -32,7 +33,7 @@ }); const update = async () => { - const result = await updateOAuth({ projectId, provider, secret, appId, enabled }); + const result = await updateOAuth({ region, projectId, provider, secret, appId, enabled }); if (result.status === 'error') { error = result.message; diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte index c086f5b55..46618c45d 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte @@ -152,7 +152,11 @@ Enable the authentication methods you wish to use. - + - - + +
{#each $authMethods.list as box} - +
{/if} - +
{/each} - +
@@ -271,3 +275,23 @@ + + diff --git a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts index 6249ef31e..68ef60d5b 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts @@ -7,6 +7,7 @@ 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; @@ -19,7 +20,302 @@ type Return = { message?: string; }; +function parseSecret(secret: string) { + if (!secret) return {}; + + try { + return JSON.parse(secret); + } catch { + return {}; + } +} + +async function updateProjectOAuth({ + region, + projectId, + provider, + appId, + secret, + enabled +}: Args) { + const projectSdk = sdk.forProject(region, projectId).project; + const parsedSecret = parseSecret(secret); + + switch (provider.key) { + case OAuthProvider.Amazon: + return projectSdk.updateOAuth2Amazon({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Apple: + return projectSdk.updateOAuth2Apple({ + serviceId: appId || undefined, + keyId: parsedSecret.keyID || undefined, + teamId: parsedSecret.teamID || undefined, + p8File: parsedSecret.p8 || undefined, + enabled + }); + case OAuthProvider.Auth0: + return projectSdk.updateOAuth2Auth0({ + clientId: appId || undefined, + clientSecret: parsedSecret.clientSecret || undefined, + endpoint: parsedSecret.auth0Domain || undefined, + enabled + }); + case OAuthProvider.Authentik: + return projectSdk.updateOAuth2Authentik({ + endpoint: parsedSecret.authentikDomain || '', + clientId: appId || undefined, + clientSecret: parsedSecret.clientSecret || undefined, + enabled + }); + case OAuthProvider.Autodesk: + return projectSdk.updateOAuth2Autodesk({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Bitbucket: + return projectSdk.updateOAuth2Bitbucket({ + key: appId || undefined, + secret: secret || undefined, + enabled + }); + case OAuthProvider.Bitly: + return projectSdk.updateOAuth2Bitly({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Box: + return projectSdk.updateOAuth2Box({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Dailymotion: + return projectSdk.updateOAuth2Dailymotion({ + apiKey: appId || undefined, + apiSecret: secret || undefined, + enabled + }); + case OAuthProvider.Discord: + return projectSdk.updateOAuth2Discord({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Disqus: + return projectSdk.updateOAuth2Disqus({ + publicKey: appId || undefined, + secretKey: secret || undefined, + enabled + }); + case OAuthProvider.Dropbox: + return projectSdk.updateOAuth2Dropbox({ + appKey: appId || undefined, + appSecret: secret || undefined, + enabled + }); + case OAuthProvider.Etsy: + return projectSdk.updateOAuth2Etsy({ + keyString: appId || undefined, + sharedSecret: secret || undefined, + enabled + }); + case OAuthProvider.Facebook: + return projectSdk.updateOAuth2Facebook({ + appId: appId || undefined, + appSecret: secret || undefined, + enabled + }); + case OAuthProvider.Figma: + return projectSdk.updateOAuth2Figma({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Fusionauth: + return projectSdk.updateOAuth2FusionAuth({ + endpoint: parsedSecret.endpoint || '', + clientId: appId || undefined, + clientSecret: parsedSecret.clientSecret || undefined, + enabled + }); + case OAuthProvider.Github: + return projectSdk.updateOAuth2GitHub({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Gitlab: + return projectSdk.updateOAuth2Gitlab({ + applicationId: appId || undefined, + secret: parsedSecret.clientSecret || undefined, + endpoint: parsedSecret.endpoint || undefined, + enabled + }); + case OAuthProvider.Google: + return projectSdk.updateOAuth2Google({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Keycloak: + return projectSdk.updateOAuth2Keycloak({ + endpoint: parsedSecret.endpoint || '', + realmName: parsedSecret.realmName || '', + clientId: appId || undefined, + clientSecret: parsedSecret.clientSecret || undefined, + enabled + }); + case OAuthProvider.Kick: + return projectSdk.updateOAuth2Kick({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Linkedin: + return projectSdk.updateOAuth2Linkedin({ + clientId: appId || undefined, + primaryClientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Microsoft: + return projectSdk.updateOAuth2Microsoft({ + tenant: parsedSecret.tenantID || 'common', + applicationId: appId || undefined, + applicationSecret: parsedSecret.clientSecret || undefined, + enabled + }); + case OAuthProvider.Notion: + return projectSdk.updateOAuth2Notion({ + oauthClientId: appId || undefined, + oauthClientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Oidc: + return projectSdk.updateOAuth2Oidc({ + clientId: appId || undefined, + clientSecret: parsedSecret.clientSecret || undefined, + wellKnownURL: parsedSecret.wellKnownEndpoint || undefined, + authorizationURL: parsedSecret.authorizationEndpoint || undefined, + tokenUrl: parsedSecret.tokenEndpoint || undefined, + userInfoUrl: parsedSecret.userinfoEndpoint || undefined, + enabled + }); + case OAuthProvider.Okta: + return projectSdk.updateOAuth2Okta({ + clientId: appId || undefined, + clientSecret: parsedSecret.clientSecret || undefined, + domain: parsedSecret.oktaDomain || undefined, + authorizationServerId: parsedSecret.authorizationServerId || undefined, + enabled + }); + case OAuthProvider.Paypal: + return projectSdk.updateOAuth2Paypal({ + clientId: appId || undefined, + secretKey: secret || undefined, + enabled + }); + case OAuthProvider.PaypalSandbox: + return projectSdk.updateOAuth2PaypalSandbox({ + clientId: appId || undefined, + secretKey: secret || undefined, + enabled + }); + case OAuthProvider.Podio: + return projectSdk.updateOAuth2Podio({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Salesforce: + return projectSdk.updateOAuth2Salesforce({ + customerKey: appId || undefined, + customerSecret: secret || undefined, + enabled + }); + case OAuthProvider.Slack: + return projectSdk.updateOAuth2Slack({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Spotify: + return projectSdk.updateOAuth2Spotify({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Stripe: + return projectSdk.updateOAuth2Stripe({ + clientId: appId || undefined, + apiSecretKey: secret || undefined, + enabled + }); + case OAuthProvider.Tradeshift: + return projectSdk.updateOAuth2Tradeshift({ + oauth2ClientId: appId || undefined, + oauth2ClientSecret: secret || undefined, + enabled + }); + case OAuthProvider.TradeshiftBox: + return projectSdk.updateOAuth2TradeshiftSandbox({ + oauth2ClientId: appId || undefined, + oauth2ClientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Twitch: + return projectSdk.updateOAuth2Twitch({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Wordpress: + return projectSdk.updateOAuth2WordPress({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.X: + return projectSdk.updateOAuth2X({ + customerKey: appId || undefined, + secretKey: secret || undefined, + enabled + }); + case OAuthProvider.Yahoo: + return projectSdk.updateOAuth2Yahoo({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Yandex: + return projectSdk.updateOAuth2Yandex({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Zoho: + return projectSdk.updateOAuth2Zoho({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + case OAuthProvider.Zoom: + return projectSdk.updateOAuth2Zoom({ + clientId: appId || undefined, + clientSecret: secret || undefined, + enabled + }); + default: + throw new Error(`Unsupported OAuth2 provider: ${provider.key}`); + } +} + export async function updateOAuth({ + region, projectId, provider, appId, @@ -31,13 +327,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, enabled }); await invalidate(Dependencies.PROJECT); addNotification({ diff --git a/static/icons/dark/color/fusionauth.svg b/static/icons/dark/color/fusionauth.svg new file mode 100644 index 000000000..00303a443 --- /dev/null +++ b/static/icons/dark/color/fusionauth.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/color/keycloak.svg b/static/icons/dark/color/keycloak.svg new file mode 100644 index 000000000..62219d304 --- /dev/null +++ b/static/icons/dark/color/keycloak.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/color/kick.svg b/static/icons/dark/color/kick.svg new file mode 100644 index 000000000..7bd713230 --- /dev/null +++ b/static/icons/dark/color/kick.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/color/oidc.svg b/static/icons/dark/color/oidc.svg index 3e45a505d..69ba83b7c 100644 --- a/static/icons/dark/color/oidc.svg +++ b/static/icons/dark/color/oidc.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/static/icons/light/color/fusionauth.svg b/static/icons/light/color/fusionauth.svg new file mode 100644 index 000000000..00303a443 --- /dev/null +++ b/static/icons/light/color/fusionauth.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/color/keycloak.svg b/static/icons/light/color/keycloak.svg new file mode 100644 index 000000000..264768ec4 --- /dev/null +++ b/static/icons/light/color/keycloak.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/color/kick.svg b/static/icons/light/color/kick.svg new file mode 100644 index 000000000..7bd713230 --- /dev/null +++ b/static/icons/light/color/kick.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/color/oidc.svg b/static/icons/light/color/oidc.svg index 3e45a505d..69ba83b7c 100644 --- a/static/icons/light/color/oidc.svg +++ b/static/icons/light/color/oidc.svg @@ -1,3 +1,5 @@ - - + + + + From 919109d4f4cf1f2baa31071f172f14cd165c01cb Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 19:22:08 +0530 Subject: [PATCH 02/27] fix: align console with latest project sdk methods --- src/lib/components/organizationUsageLimits.svelte | 14 ++++++++++---- .../sonners/{error.svelte => errorSonner.svelte} | 0 .../(components)/sonners/index.ts | 2 +- .../overview/(components)/create.svelte | 2 +- .../settings/deleteProject.svelte | 4 +--- .../settings/migrations/+page.svelte | 2 +- .../settings/migrations/exportModal.svelte | 2 +- 7 files changed, 15 insertions(+), 11 deletions(-) rename src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/{error.svelte => errorSonner.svelte} (100%) diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index 39ae9a1ea..ae8b08b7f 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -90,10 +90,16 @@ } 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( diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/error.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/errorSonner.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/error.svelte rename to src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/errorSonner.svelte diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts index cc1daf60a..f5afd0bc5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts @@ -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'; diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte index ed56fe2d0..59b8565c8 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte @@ -30,7 +30,7 @@ try { const { $id, secret } = await sdk .forProject(page.params.region, page.params.project) - .project.createKey({ + .project.createStandardKey({ keyId: ID.unique(), name, scopes, diff --git a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte index a39f59bf6..ba12f9b94 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte @@ -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; diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte index 7bed364e6..0a19e9336 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte @@ -88,7 +88,7 @@ // Create API key const { secret } = await sdk .forProject(page.params.region, page.params.project) - .project.createKey({ + .project.createStandardKey({ keyId: ID.unique(), name: `[AUTO-GENERATED] Migration ${new Date().toISOString()}`, scopes: [ diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte index d2e717718..aa45020b3 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte @@ -90,7 +90,7 @@ // Create API key const { secret } = await sdk .forProject(page.params.region, page.params.project) - .project.createKey({ + .project.createStandardKey({ keyId: ID.unique(), name: `[AUTO-GENERATED] Migration ${new Date().toISOString()}`, scopes: [ From 14154cb5278512ffa2f97c6bf103d0975e443a5a Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 19:24:45 +0530 Subject: [PATCH 03/27] format --- src/lib/components/organizationUsageLimits.svelte | 4 +--- .../auth/(providers)/fusionauthOAuth.svelte | 3 ++- .../auth/settings/+page.svelte | 1 - .../project-[region]-[project]/auth/updateOAuth.ts | 9 +-------- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index ae8b08b7f..8373b17b3 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -95,9 +95,7 @@ return { projectId, - promise: sdk - .forProject(projectToDelete.region, projectId) - .project.delete() + promise: sdk.forProject(projectToDelete.region, projectId).project.delete() }; }); diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte index f74a0f593..d50993b06 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte @@ -43,7 +43,8 @@ } }; - $: secret = clientSecret && endpoint ? JSON.stringify({ clientSecret, endpoint }) : provider.secret; + $: secret = + clientSecret && endpoint ? JSON.stringify({ clientSecret, endpoint }) : provider.secret; diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte index 46618c45d..1b04701fc 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte @@ -288,7 +288,6 @@ min-width: 0; } - @media (max-width: 768px) { .auth-methods-grid { grid-template-columns: 1fr; diff --git a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts index 68ef60d5b..8a9e29d03 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts @@ -30,14 +30,7 @@ function parseSecret(secret: string) { } } -async function updateProjectOAuth({ - region, - projectId, - provider, - appId, - secret, - enabled -}: Args) { +async function updateProjectOAuth({ region, projectId, provider, appId, secret, enabled }: Args) { const projectSdk = sdk.forProject(region, projectId).project; const parsedSecret = parseSecret(secret); From 75c34eb4850d4904b97d02f8f854e0a5a5c986d3 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 19:46:24 +0530 Subject: [PATCH 04/27] fix: remove unsupported yammer oauth provider --- src/lib/stores/oauth-providers.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index cffd6fd41..7e9249924 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -268,12 +268,6 @@ export const oAuthProviders: Record = { 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', From 8a78402ea17d864c80470be9635af84707337cf6 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 20:05:04 +0530 Subject: [PATCH 05/27] remove yammer and change names --- bun.lock | 4 +-- package.json | 2 +- .../overview/(components)/create.svelte | 2 +- .../settings/migrations/+page.svelte | 4 +-- .../settings/migrations/exportModal.svelte | 4 +-- static/icons/dark/color/yammer.svg | 26 ------------------- static/icons/dark/grayscale/yammer.svg | 7 ----- static/icons/light/color/yammer.svg | 26 ------------------- static/icons/light/grayscale/yammer.svg | 7 ----- 9 files changed, 8 insertions(+), 74 deletions(-) delete mode 100644 static/icons/dark/color/yammer.svg delete mode 100644 static/icons/dark/grayscale/yammer.svg delete mode 100644 static/icons/light/color/yammer.svg delete mode 100644 static/icons/light/grayscale/yammer.svg diff --git a/bun.lock b/bun.lock index dacb63cc5..3e2c925ae 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d10b661", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@33968aa", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", @@ -124,7 +124,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@d10b661", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@33968aa", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index 7efc7474e..a18b34eaa 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d10b661", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@33968aa", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte index 59b8565c8..ed56fe2d0 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte @@ -30,7 +30,7 @@ try { const { $id, secret } = await sdk .forProject(page.params.region, page.params.project) - .project.createStandardKey({ + .project.createKey({ keyId: ID.unique(), name, scopes, diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte index 0a19e9336..fc54b8ba6 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/+page.svelte @@ -88,7 +88,7 @@ // Create API key const { secret } = await sdk .forProject(page.params.region, page.params.project) - .project.createStandardKey({ + .project.createKey({ keyId: ID.unique(), name: `[AUTO-GENERATED] Migration ${new Date().toISOString()}`, scopes: [ @@ -105,7 +105,7 @@ Scopes.FilesRead, Scopes.BucketsRead, Scopes.FunctionsRead, - Scopes.ExecutionRead, + Scopes.ExecutionsRead, Scopes.LocaleRead, Scopes.AvatarsRead, Scopes.HealthRead diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte index aa45020b3..e70702127 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte @@ -90,7 +90,7 @@ // Create API key const { secret } = await sdk .forProject(page.params.region, page.params.project) - .project.createStandardKey({ + .project.createKey({ keyId: ID.unique(), name: `[AUTO-GENERATED] Migration ${new Date().toISOString()}`, scopes: [ @@ -107,7 +107,7 @@ Scopes.FilesRead, Scopes.BucketsRead, Scopes.FunctionsRead, - Scopes.ExecutionRead, + Scopes.ExecutionsRead, Scopes.SitesRead, Scopes.ProvidersRead, Scopes.TopicsRead, diff --git a/static/icons/dark/color/yammer.svg b/static/icons/dark/color/yammer.svg deleted file mode 100644 index 2d1083bd9..000000000 --- a/static/icons/dark/color/yammer.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/icons/dark/grayscale/yammer.svg b/static/icons/dark/grayscale/yammer.svg deleted file mode 100644 index cbb07a3c2..000000000 --- a/static/icons/dark/grayscale/yammer.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/static/icons/light/color/yammer.svg b/static/icons/light/color/yammer.svg deleted file mode 100644 index e6d34f1e9..000000000 --- a/static/icons/light/color/yammer.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/icons/light/grayscale/yammer.svg b/static/icons/light/grayscale/yammer.svg deleted file mode 100644 index 66e6ba653..000000000 --- a/static/icons/light/grayscale/yammer.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - From ed8bd04d9065cebfcb07e4df5a5c4638dd6a7d7a Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 21:46:52 +0530 Subject: [PATCH 06/27] feat: dynamic OAuth2 forms and API key scopes from console SDK - Replace hardcoded OAuth2 provider components with a single dynamic mainOAuth.svelte that fetches labels, placeholders, and hints from listOAuth2Providers; removes 10 now-unused provider-specific files - Load OAuth2 provider list and enabled state from project SDK in +page.ts instead of reading from the project model directly - Fetch API key scopes from listProjectScopes instead of static constants; migrate scopes.svelte to Svelte 5 runes - Add executions.read/write scope definitions and legacy compat pair for execution. -> executions. - Secret field uses write-only card pattern (Tag -> expandable Card); no pre-fill, no silent fallbacks --- src/lib/constants.ts | 22 +- src/lib/stores/oauth-providers.ts | 32 +-- .../auth/(providers)/appleOAuth.svelte | 87 ------- .../auth/(providers)/auth0OAuth.svelte | 92 -------- .../auth/(providers)/authentikOAuth.svelte | 96 -------- .../auth/(providers)/fusionauthOAuth.svelte | 98 -------- .../auth/(providers)/gitlabOAuth.svelte | 85 ------- .../auth/(providers)/googleOAuth.svelte | 85 ------- .../auth/(providers)/keycloakOAuth.svelte | 108 --------- .../auth/(providers)/mainOAuth.svelte | 212 +++++++++++++++--- .../auth/(providers)/microsoftOAuth.svelte | 91 -------- .../auth/(providers)/oidcOAuth.svelte | 134 ----------- .../auth/(providers)/oktaOAuth.svelte | 110 --------- .../auth/settings/+page.svelte | 5 +- .../auth/settings/+page.ts | 45 ++++ .../overview/api-keys/scopes.svelte | 92 ++++---- 16 files changed, 308 insertions(+), 1086 deletions(-) delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte delete mode 100644 src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts diff --git a/src/lib/constants.ts b/src/lib/constants.ts index be661eefb..d917925ae 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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", diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index 7e9249924..4870b4ccc 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -1,15 +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 FusionAuth from '$routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.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 Keycloak from '$routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.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; @@ -30,19 +20,19 @@ export const oAuthProviders: Record = { 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', @@ -114,7 +104,7 @@ export const oAuthProviders: Record = { name: 'FusionAuth', icon: 'fusionauth', docs: 'https://fusionauth.io/docs/apis/identity-providers/oauth2', - component: FusionAuth + component: Main }, github: { name: 'GitHub', @@ -133,26 +123,26 @@ export const oAuthProviders: Record = { 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: Keycloak + component: Main }, kick: { name: 'Kick', @@ -170,7 +160,7 @@ export const oAuthProviders: Record = { name: 'Microsoft', icon: 'microsoft', docs: 'https://developer.microsoft.com/en-us/', - component: Microsoft + component: Main }, notion: { name: 'Notion', @@ -182,13 +172,13 @@ export const oAuthProviders: Record = { 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', diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte deleted file mode 100644 index 34aa8c847..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/appleOAuth.svelte +++ /dev/null @@ -1,87 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - - visit the docs. - -

- - - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte deleted file mode 100644 index fb65dadd4..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/auth0OAuth.svelte +++ /dev/null @@ -1,92 +0,0 @@ - - - - - To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. - - - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte deleted file mode 100644 index 0b65d7c3b..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/authentikOAuth.svelte +++ /dev/null @@ -1,96 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. -

- - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte deleted file mode 100644 index d50993b06..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/fusionauthOAuth.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. -

- - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte deleted file mode 100644 index 025f2419e..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/gitlabOAuth.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. -

- - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte deleted file mode 100644 index 05e5560f2..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - - - {provider.name} OAuth2 settings - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. -

- - - - - 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. - - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte deleted file mode 100644 index 217838736..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/keycloakOAuth.svelte +++ /dev/null @@ -1,108 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. -

- - - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index 44109091b..cf45501c4 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -1,13 +1,31 @@ @@ -50,21 +128,99 @@ target="_blank" rel="noopener noreferrer">visit the docs.

- - - + + {#if loading} + + + + {:else} + + + {#if appIdParam} + + {/if} + + {#if secretParams.length > 0} + {#if !showSecretInput} +
+ (showSecretInput = true)}> + Update {secretCardTitle} + +
+ {:else} + + + + + {secretCardTitle} + + + + {#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} + + + + + + + {#each secretParams as param} + {#if isTextareaParam(param.$id)} + + {:else if isPasswordParam(param.$id)} + + {:else} + + {/if} + {/each} + + + + {/if} + {/if} + {/if} + To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. @@ -73,12 +229,6 @@ value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} /> - +
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte deleted file mode 100644 index d192a3b97..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/microsoftOAuth.svelte +++ /dev/null @@ -1,91 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - - visit the docs. - -

- - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte deleted file mode 100644 index 377ddefa1..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oidcOAuth.svelte +++ /dev/null @@ -1,134 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. -

- - - - - - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte deleted file mode 100644 index 069211370..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/oktaOAuth.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - - -

- To use {provider.name} authentication in your application, first fill in this form. For more info - you can - visit the docs. -

- - - - - - - - To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration. - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte index 1b04701fc..c314cf7b6 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte @@ -30,6 +30,7 @@ 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); let showProvider = $state(false); let selectedProvider: Models.AuthProvider | null = $state(null); @@ -204,8 +205,8 @@ OAuth2 Providers
    - {#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} diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts new file mode 100644 index 000000000..7a4f2a743 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts @@ -0,0 +1,45 @@ +import { Dependencies } from '$lib/constants'; +import { oAuthProviders } from '$lib/stores/oauth-providers'; +import { sdk } from '$lib/stores/sdk'; +import type { Models } from '@appwrite.io/console'; +import type { PageLoad } from './$types'; + +/** Extract the appId equivalent from a typed OAuth2 provider model. */ +function extractAppId(p: Record): 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 = await sdk + .forProject(params.region, params.project) + .project.listOAuth2Providers(); + + const oauthProviders: Models.AuthProvider[] = providerList.providers + .filter((p) => p.$id !== 'mock') + .map((p) => { + const raw = p as unknown as Record; + return { + key: p.$id, + name: oAuthProviders[p.$id]?.name ?? p.$id, + appId: extractAppId(raw), + secret: '', + enabled: p.enabled + }; + }); + + return { oauthProviders }; +}; diff --git a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte index 70608e2f0..9d146ddab 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte @@ -1,8 +1,9 @@ - From 4e55de11873df1ef0a86386ef3f620349c01f033 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 21:55:17 +0530 Subject: [PATCH 07/27] fix: align updateOAuth secret key names with console API parameter IDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - authentikDomain → endpoint (Authentik) - auth0Domain → endpoint (Auth0) - tenantID → tenant, clientSecret → applicationSecret (Microsoft) - oktaDomain → domain (Okta) - keyID/teamID → keyId/teamId (Apple) - wellKnownEndpoint/authorizationEndpoint/tokenEndpoint/userinfoEndpoint → wellKnownURL/authorizationURL/tokenUrl/userInfoUrl (OIDC) --- .../auth/updateOAuth.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts index 8a9e29d03..63741d3da 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts @@ -44,8 +44,8 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, case OAuthProvider.Apple: return projectSdk.updateOAuth2Apple({ serviceId: appId || undefined, - keyId: parsedSecret.keyID || undefined, - teamId: parsedSecret.teamID || undefined, + keyId: parsedSecret.keyId || undefined, + teamId: parsedSecret.teamId || undefined, p8File: parsedSecret.p8 || undefined, enabled }); @@ -53,12 +53,12 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, return projectSdk.updateOAuth2Auth0({ clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, - endpoint: parsedSecret.auth0Domain || undefined, + endpoint: parsedSecret.endpoint || undefined, enabled }); case OAuthProvider.Authentik: return projectSdk.updateOAuth2Authentik({ - endpoint: parsedSecret.authentikDomain || '', + endpoint: parsedSecret.endpoint || '', clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, enabled @@ -177,9 +177,9 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, }); case OAuthProvider.Microsoft: return projectSdk.updateOAuth2Microsoft({ - tenant: parsedSecret.tenantID || 'common', + tenant: parsedSecret.tenant || 'common', applicationId: appId || undefined, - applicationSecret: parsedSecret.clientSecret || undefined, + applicationSecret: parsedSecret.applicationSecret || undefined, enabled }); case OAuthProvider.Notion: @@ -192,17 +192,17 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, return projectSdk.updateOAuth2Oidc({ clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, - wellKnownURL: parsedSecret.wellKnownEndpoint || undefined, - authorizationURL: parsedSecret.authorizationEndpoint || undefined, - tokenUrl: parsedSecret.tokenEndpoint || undefined, - userInfoUrl: parsedSecret.userinfoEndpoint || undefined, + wellKnownURL: parsedSecret.wellKnownURL || undefined, + authorizationURL: parsedSecret.authorizationURL || undefined, + tokenUrl: parsedSecret.tokenUrl || undefined, + userInfoUrl: parsedSecret.userInfoUrl || undefined, enabled }); case OAuthProvider.Okta: return projectSdk.updateOAuth2Okta({ clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, - domain: parsedSecret.oktaDomain || undefined, + domain: parsedSecret.domain || undefined, authorizationServerId: parsedSecret.authorizationServerId || undefined, enabled }); From 838d6029d2ac02cad40fc470c47a6a1919378882 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Wed, 29 Apr 2026 21:57:49 +0530 Subject: [PATCH 08/27] fix: correct OAuth2 field name mismatches found from API response - Apple: p8 -> p8File (API field is p8File) - GitLab: clientSecret -> secret (API field is secret) - oauth-providers: paypalsandbox -> paypalSandbox (matches API $id) --- src/lib/stores/oauth-providers.ts | 2 +- .../(console)/project-[region]-[project]/auth/updateOAuth.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index 4870b4ccc..1a2143a17 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -186,7 +186,7 @@ export const oAuthProviders: Record = { 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/', diff --git a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts index 63741d3da..0ae46d03d 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts @@ -46,7 +46,7 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, serviceId: appId || undefined, keyId: parsedSecret.keyId || undefined, teamId: parsedSecret.teamId || undefined, - p8File: parsedSecret.p8 || undefined, + p8File: parsedSecret.p8File || undefined, enabled }); case OAuthProvider.Auth0: @@ -145,7 +145,7 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, case OAuthProvider.Gitlab: return projectSdk.updateOAuth2Gitlab({ applicationId: appId || undefined, - secret: parsedSecret.clientSecret || undefined, + secret: parsedSecret.secret || undefined, endpoint: parsedSecret.endpoint || undefined, enabled }); From 0001eb98289be8520ca6375474f060a5403f6b2d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 14:37:12 +0530 Subject: [PATCH 09/27] perf: prefetch OAuth2 provider params on page load, remove modal loader Fetch listOAuth2Providers alongside listOAuth2Providers (project) in +page.ts using Promise.all, pass parameters as a prop to mainOAuth. Modal now opens instantly with no spinner or async fetch. --- .../auth/(providers)/mainOAuth.svelte | 221 ++++++++---------- .../auth/settings/+page.svelte | 2 + .../auth/settings/+page.ts | 16 +- 3 files changed, 112 insertions(+), 127 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index cf45501c4..053190a37 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -8,10 +8,9 @@ InputText, InputTextarea } from '$lib/elements/forms'; - import { onMount } from 'svelte'; import { updateOAuth } from '../updateOAuth'; - import type { Models } from '@appwrite.io/console'; - import { oAuthProviders, type Provider } from '$lib/stores/oauth-providers'; + import type { Models, Models as ConsoleModels } from '@appwrite.io/console'; + import { oAuthProviders } from '$lib/stores/oauth-providers'; import { Alert, Card, @@ -19,30 +18,37 @@ Icon, Layout, Link, - Spinner, Tag, Typography } from '@appwrite.io/pink-svelte'; import { IconPencil, IconX } from '@appwrite.io/pink-icons-svelte'; - import { getApiEndpoint, sdk } from '$lib/stores/sdk'; - import type { Models as ConsoleModels } from '@appwrite.io/console'; + 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 loading = true; - let enabled: boolean = null; - let appId: string = null; + $: appIdParam = parameters.length >= 1 ? parameters[0] : null; + $: secretParams = parameters.slice(1); + + let enabled: boolean = provider.enabled; + let appId: string = provider.appId || null; /** Values for all non-appId parameters, keyed by parameter.$id */ let secretValues: Record = {}; - let showSecretInput = false; - let oAuthProvider: Provider; + let showSecretInput = !provider.appId; - let appIdParam: ConsoleModels.ConsoleOAuth2ProviderParameter | null = null; - let secretParams: ConsoleModels.ConsoleOAuth2ProviderParameter[] = []; + $: { + for (const p of secretParams) { + if (!(p.$id in secretValues)) { + secretValues[p.$id] = null; + } + } + } + + $: oAuthProvider = oAuthProviders[provider.key]; /** Take only the primary name before any " or " / ", or " alternatives. */ function primaryName(name: string): string { @@ -57,29 +63,6 @@ return id === 'p8' || id.toLowerCase().includes('p8file'); } - onMount(async () => { - oAuthProvider = oAuthProviders[provider.key]; - - const providerList = await sdk.forConsole.console.listOAuth2Providers(); - const consoleProvider = providerList.oAuth2Providers.find((p) => p.$id === provider.key); - - if (consoleProvider?.parameters?.length >= 1) { - appIdParam = consoleProvider.parameters[0]; - secretParams = consoleProvider.parameters.slice(1); - } - - // Initialise all secret value slots - for (const p of secretParams) { - secretValues[p.$id] = null; - } - - // Initialise form values only after params are ready — no flash - enabled = provider.enabled; - appId = provider.appId || null; - showSecretInput = !provider.appId; - loading = false; - }); - /** Build the secret string to pass to updateOAuth. */ function buildSecret(): string | null { const entries = secretParams @@ -129,95 +112,89 @@ rel="noopener noreferrer">visit the docs.

    - {#if loading} - - - - {:else} - + - {#if appIdParam} - - {/if} + {#if appIdParam} + + {/if} - {#if secretParams.length > 0} - {#if !showSecretInput} -
    - (showSecretInput = true)}> - Update {secretCardTitle} - -
    - {:else} - - - - - {secretCardTitle} - - - - {#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} - - - - - - - {#each secretParams as param} - {#if isTextareaParam(param.$id)} - - {:else if isPasswordParam(param.$id)} - - {:else} - - {/if} - {/each} + {#if secretParams.length > 0} + {#if !showSecretInput} +
    + (showSecretInput = true)}> + Update {secretCardTitle} + +
    + {:else} + + + + + {secretCardTitle} + + + {#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} + - - {/if} + + + + + {#each secretParams as param} + {#if isTextareaParam(param.$id)} + + {:else if isPasswordParam(param.$id)} + + {:else} + + {/if} + {/each} + +
    +
    {/if} {/if} @@ -229,6 +206,6 @@ value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} /> - + diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte index c314cf7b6..3596e8d7e 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte @@ -31,6 +31,7 @@ /** 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); @@ -253,6 +254,7 @@ { selectedProvider = null; showProvider = false; diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts index 7a4f2a743..1040df0e4 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts @@ -1,7 +1,7 @@ import { Dependencies } from '$lib/constants'; import { oAuthProviders } from '$lib/stores/oauth-providers'; import { sdk } from '$lib/stores/sdk'; -import type { Models } from '@appwrite.io/console'; +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. */ @@ -24,9 +24,15 @@ function extractAppId(p: Record): string { export const load: PageLoad = async ({ depends, params }) => { depends(Dependencies.PROJECT); - const providerList = await sdk - .forProject(params.region, params.project) - .project.listOAuth2Providers(); + const [providerList, consoleProviderList] = await Promise.all([ + sdk.forProject(params.region, params.project).project.listOAuth2Providers(), + sdk.forConsole.console.listOAuth2Providers() + ]); + + const consoleParamsMap = new Map(); + for (const cp of consoleProviderList.oAuth2Providers) { + consoleParamsMap.set(cp.$id, cp.parameters ?? []); + } const oauthProviders: Models.AuthProvider[] = providerList.providers .filter((p) => p.$id !== 'mock') @@ -41,5 +47,5 @@ export const load: PageLoad = async ({ depends, params }) => { }; }); - return { oauthProviders }; + return { oauthProviders, consoleParamsMap }; }; From e7810c324abf83201effafb19a7476e2aead46fd Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 14:41:42 +0530 Subject: [PATCH 10/27] feat: replace Apple p8 textarea with file upload zone p8File fields now show a drag-and-drop style upload zone by default. Users can click to upload a .p8 file directly or toggle to paste mode for manual entry. Loaded files show a confirmation state with a clear button. --- .../auth/(providers)/mainOAuth.svelte | 105 ++++++++++++++++-- 1 file changed, 95 insertions(+), 10 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index 053190a37..3f4e82617 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -21,7 +21,7 @@ Tag, Typography } from '@appwrite.io/pink-svelte'; - import { IconPencil, IconX } from '@appwrite.io/pink-icons-svelte'; + import { IconDocument, IconPencil, IconUpload, IconX } from '@appwrite.io/pink-icons-svelte'; import { getApiEndpoint } from '$lib/stores/sdk'; const projectId = page.params.project; @@ -59,8 +59,18 @@ return id.toLowerCase().includes('secret'); } - function isTextareaParam(id: string): boolean { - return id === 'p8' || id.toLowerCase().includes('p8file'); + function isP8Param(id: string): boolean { + return id === 'p8File' || id === 'p8' || id.toLowerCase().includes('p8file'); + } + + let p8PasteMode: Record = {}; + + async function handleP8FileUpload(id: string, event: Event) { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file) return; + secretValues[id] = await file.text(); + input.value = ''; } /** Build the secret string to pass to updateOAuth. */ @@ -168,13 +178,64 @@ {#each secretParams as param} - {#if isTextareaParam(param.$id)} - + {#if isP8Param(param.$id)} +
    + + {primaryName(param.name)} + + + {#if p8PasteMode[param.$id]} + + {:else} + + handleP8FileUpload(param.$id, e)} /> + {/if} +
    {:else if isPasswordParam(param.$id)} Update + + From 5686812887ad260f3dfd0c24cb18f340eed14578 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 15:43:06 +0530 Subject: [PATCH 11/27] fix-11993-oauth-provider-form-behavior --- .../auth/(providers)/mainOAuth.svelte | 234 ++++++++++++------ .../auth/settings/+page.ts | 1 + .../auth/updateOAuth.ts | 46 ++-- 3 files changed, 195 insertions(+), 86 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index 3f4e82617..efaa03c7c 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -9,9 +9,10 @@ InputTextarea } from '$lib/elements/forms'; import { updateOAuth } from '../updateOAuth'; - import type { Models, Models as ConsoleModels } from '@appwrite.io/console'; + import { OAuthProvider, type Models, type Models as ConsoleModels } from '@appwrite.io/console'; import { oAuthProviders } from '$lib/stores/oauth-providers'; import { + Accordion, Alert, Card, Divider, @@ -31,28 +32,69 @@ export let show = false; export let parameters: ConsoleModels.ConsoleOAuth2ProviderParameter[] = []; - $: appIdParam = parameters.length >= 1 ? parameters[0] : null; - $: secretParams = parameters.slice(1); - let enabled: boolean = provider.enabled; let appId: string = provider.appId || null; - /** Values for all non-appId parameters, keyed by parameter.$id */ - let secretValues: Record = {}; - let showSecretInput = !provider.appId; + let providerKeyId = (provider as Models.AuthProvider & { keyId?: string })?.keyId || null; + let fieldValues: Record = {}; + let showSecretInput = + !provider.appId || (provider.key === OAuthProvider.Apple && !providerKeyId); + let p8PasteMode: Record = {}; + let error: 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) => { + const value = fieldValues[param.$id]; + return value !== null && value !== ''; + }); + $: nothingChanged = + enabled === provider?.enabled && + appId === provider?.appId && + !hasSecretInput && + !hasDetailChanges; + $: oAuthProvider = oAuthProviders[provider.key]; + $: secretCardTitle = + secretParams.length === 1 ? primaryName(secretParams[0]?.name ?? '') : 'Credentials'; $: { - for (const p of secretParams) { - if (!(p.$id in secretValues)) { - secretValues[p.$id] = null; + for (const param of additionalParams) { + if (!(param.$id in fieldValues)) { + fieldValues[param.$id] = getInitialFieldValue(param.$id); } } } - $: oAuthProvider = oAuthProviders[provider.key]; - - /** Take only the primary name before any " or " / ", or " alternatives. */ function primaryName(name: string): string { - return name.split(/,?\s+or\s+/i)[0].trim(); + return name.trim(); + } + + function helperText(hint?: string | null): string | undefined { + const value = hint?.trim(); + + if (!value || /^example of wrong value:/i.test(value)) { + return undefined; + } + + return value; + } + + function getInitialFieldValue(id: string): string | null { + const value = (provider as Record)[id]; + return typeof value === 'string' ? value : null; } function isPasswordParam(id: string): boolean { @@ -63,32 +105,53 @@ return id === 'p8File' || id === 'p8' || id.toLowerCase().includes('p8file'); } - let p8PasteMode: Record = {}; + 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; - secretValues[id] = await file.text(); + + fieldValues[id] = await file.text(); input.value = ''; } - /** Build the secret string to pass to updateOAuth. */ function buildSecret(): string | null { - const entries = secretParams - .map((p) => [p.$id, secretValues[p.$id]] as [string, string | null]) - .filter(([, v]) => v !== null && v !== ''); + 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; if (secretParams.length === 1) return entries[0]?.[1] ?? null; + return JSON.stringify(Object.fromEntries(entries)); } - $: hasSecretInput = Object.values(secretValues).some((v) => v !== null && v !== ''); - $: nothingChanged = - enabled === provider?.enabled && appId === provider?.appId && !hasSecretInput; + function buildDetails(): Record { + return Object.fromEntries( + detailParams.map((param) => [param.$id, fieldValues[param.$id] ?? '']) + ); + } - let error: string; + function resetWriteOnlyInputs() { + showSecretInput = false; + + for (const param of secretParams) { + fieldValues[param.$id] = null; + } + } const update = async () => { const result = await updateOAuth({ @@ -97,6 +160,7 @@ provider, appId, secret: buildSecret(), + details: buildDetails(), enabled }); @@ -107,8 +171,7 @@ } }; - $: secretCardTitle = - secretParams.length === 1 ? primaryName(secretParams[0]?.name ?? '') : 'Credentials'; + // @todo Revisit explicit secret-clearing UX later today. For now, credentials remain update-only. @@ -130,10 +193,34 @@ label={primaryName(appIdParam.name)} autofocus={true} placeholder={appIdParam.example || ''} - helper={appIdParam.hint || undefined} + helper={helperText(appIdParam.hint)} bind:value={appId} /> {/if} + {#each basicDetailParams as param} + + {/each} + + {#if advancedDetailParams.length > 0} + + + {#each advancedDetailParams as param} + + {/each} + + + {/if} + {#if secretParams.length > 0} {#if !showSecretInput}
    @@ -153,14 +240,7 @@ justifyContent="space-between" alignContent="center"> {secretCardTitle} - @@ -179,46 +259,46 @@ {#each secretParams as param} {#if isP8Param(param.$id)} -
    - +
    +
    {primaryName(param.name)} - - +
    + +
    +
    {#if p8PasteMode[param.$id]} + helper={helperText(param.hint)} + bind:value={fieldValues[param.$id]} /> {:else}
    - {:else if isPasswordParam(param.$id)} + {:else} - {:else} - + bind:value={fieldValues[param.$id]} /> {/if} {/each}
    @@ -293,4 +367,26 @@ 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; + } diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts index 1040df0e4..0aa058f7e 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.ts @@ -39,6 +39,7 @@ export const load: PageLoad = async ({ depends, params }) => { .map((p) => { const raw = p as unknown as Record; return { + ...raw, key: p.$id, name: oAuthProviders[p.$id]?.name ?? p.$id, appId: extractAppId(raw), diff --git a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts index 0ae46d03d..55c1af5ea 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts @@ -12,6 +12,7 @@ type Args = { provider: Models.AuthProvider; appId: string; secret: string; + details: Record; enabled: boolean; }; @@ -30,10 +31,20 @@ function parseSecret(secret: string) { } } -async function updateProjectOAuth({ region, projectId, provider, appId, secret, enabled }: Args) { +async function updateProjectOAuth({ + region, + projectId, + provider, + appId, + secret, + details, + enabled +}: Args) { const projectSdk = sdk.forProject(region, projectId).project; const parsedSecret = parseSecret(secret); + const getDetail = (key: string): string => details[key] ?? ''; + switch (provider.key) { case OAuthProvider.Amazon: return projectSdk.updateOAuth2Amazon({ @@ -44,8 +55,8 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, case OAuthProvider.Apple: return projectSdk.updateOAuth2Apple({ serviceId: appId || undefined, - keyId: parsedSecret.keyId || undefined, - teamId: parsedSecret.teamId || undefined, + keyId: getDetail('keyId'), + teamId: getDetail('teamId'), p8File: parsedSecret.p8File || undefined, enabled }); @@ -53,12 +64,12 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, return projectSdk.updateOAuth2Auth0({ clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, - endpoint: parsedSecret.endpoint || undefined, + endpoint: getDetail('endpoint'), enabled }); case OAuthProvider.Authentik: return projectSdk.updateOAuth2Authentik({ - endpoint: parsedSecret.endpoint || '', + endpoint: getDetail('endpoint'), clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, enabled @@ -131,7 +142,7 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, }); case OAuthProvider.Fusionauth: return projectSdk.updateOAuth2FusionAuth({ - endpoint: parsedSecret.endpoint || '', + endpoint: getDetail('endpoint'), clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, enabled @@ -146,7 +157,7 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, return projectSdk.updateOAuth2Gitlab({ applicationId: appId || undefined, secret: parsedSecret.secret || undefined, - endpoint: parsedSecret.endpoint || undefined, + endpoint: getDetail('endpoint'), enabled }); case OAuthProvider.Google: @@ -157,8 +168,8 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, }); case OAuthProvider.Keycloak: return projectSdk.updateOAuth2Keycloak({ - endpoint: parsedSecret.endpoint || '', - realmName: parsedSecret.realmName || '', + endpoint: getDetail('endpoint'), + realmName: getDetail('realmName'), clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, enabled @@ -177,7 +188,7 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, }); case OAuthProvider.Microsoft: return projectSdk.updateOAuth2Microsoft({ - tenant: parsedSecret.tenant || 'common', + tenant: getDetail('tenant'), applicationId: appId || undefined, applicationSecret: parsedSecret.applicationSecret || undefined, enabled @@ -192,18 +203,18 @@ async function updateProjectOAuth({ region, projectId, provider, appId, secret, return projectSdk.updateOAuth2Oidc({ clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, - wellKnownURL: parsedSecret.wellKnownURL || undefined, - authorizationURL: parsedSecret.authorizationURL || undefined, - tokenUrl: parsedSecret.tokenUrl || undefined, - userInfoUrl: parsedSecret.userInfoUrl || undefined, + wellKnownURL: getDetail('wellKnownURL'), + authorizationURL: getDetail('authorizationURL'), + tokenUrl: getDetail('tokenUrl'), + userInfoUrl: getDetail('userInfoUrl'), enabled }); case OAuthProvider.Okta: return projectSdk.updateOAuth2Okta({ clientId: appId || undefined, clientSecret: parsedSecret.clientSecret || undefined, - domain: parsedSecret.domain || undefined, - authorizationServerId: parsedSecret.authorizationServerId || undefined, + domain: getDetail('domain'), + authorizationServerId: getDetail('authorizationServerId'), enabled }); case OAuthProvider.Paypal: @@ -313,6 +324,7 @@ export async function updateOAuth({ provider, appId, secret, + details, enabled }: Args): Promise { try { @@ -320,7 +332,7 @@ export async function updateOAuth({ throw new Error(`Invalid OAuth2 provider: ${provider.key}`); } - await updateProjectOAuth({ region, projectId, provider, appId, secret, enabled }); + await updateProjectOAuth({ region, projectId, provider, appId, secret, details, enabled }); await invalidate(Dependencies.PROJECT); addNotification({ From 2301a005637c62b42801354e645bcf97b2c56343 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 15:47:59 +0530 Subject: [PATCH 12/27] fix-11993-oauth-modal-dirty-state --- .../auth/(providers)/mainOAuth.svelte | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index efaa03c7c..9f3962390 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -33,13 +33,16 @@ export let parameters: ConsoleModels.ConsoleOAuth2ProviderParameter[] = []; let enabled: boolean = provider.enabled; - let appId: string = provider.appId || null; - let providerKeyId = (provider as Models.AuthProvider & { keyId?: string })?.keyId || null; + let appId: string | null = null; + let providerKeyId: string | null = null; let fieldValues: Record = {}; - let showSecretInput = - !provider.appId || (provider.key === OAuthProvider.Apple && !providerKeyId); + let showSecretInput = false; let p8PasteMode: Record = {}; let error: string; + let initializedProvider: Models.AuthProvider | null = null; + let initialEnabled = false; + let initialAppId: string | null = null; + let initialDetailValues: Record = {}; $: appIdParam = parameters.length >= 1 ? parameters[0] : null; $: additionalParams = parameters.slice(1); @@ -58,24 +61,34 @@ return value !== null && value !== ''; }); $: hasDetailChanges = detailParams.some((param) => { - const value = fieldValues[param.$id]; - return value !== null && value !== ''; + return normalizeFieldValue(fieldValues[param.$id]) !== (initialDetailValues[param.$id] ?? ''); }); $: nothingChanged = - enabled === provider?.enabled && - appId === provider?.appId && + enabled === initialEnabled && + normalizeFieldValue(appId) === normalizeFieldValue(initialAppId) && !hasSecretInput && !hasDetailChanges; $: oAuthProvider = oAuthProviders[provider.key]; $: secretCardTitle = secretParams.length === 1 ? primaryName(secretParams[0]?.name ?? '') : 'Credentials'; - $: { - for (const param of additionalParams) { - if (!(param.$id in fieldValues)) { - fieldValues[param.$id] = getInitialFieldValue(param.$id); - } - } + $: 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 { @@ -97,6 +110,29 @@ return typeof value === 'string' ? value : null; } + function getInitialAppId(): string | null { + const raw = provider as Record; + 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'); } From 16060fbeb55162bb6706cb4bd6b73fd734108971 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 15:50:57 +0530 Subject: [PATCH 13/27] fix-11993-restore-oauth-helper-text --- .../auth/(providers)/mainOAuth.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index 9f3962390..d398cb523 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -98,7 +98,7 @@ function helperText(hint?: string | null): string | undefined { const value = hint?.trim(); - if (!value || /^example of wrong value:/i.test(value)) { + if (!value) { return undefined; } From 5654f12dddc21523477a5ac0919ea185849a4093 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 15:57:37 +0530 Subject: [PATCH 14/27] fix-11993-clear-non-secret-oauth-fields --- .../auth/updateOAuth.ts | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts index 55c1af5ea..6bdb87a4b 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts @@ -10,7 +10,7 @@ type Args = { region: string; projectId: string; provider: Models.AuthProvider; - appId: string; + appId: string | null; secret: string; details: Record; enabled: boolean; @@ -43,18 +43,19 @@ async function updateProjectOAuth({ const projectSdk = sdk.forProject(region, projectId).project; const parsedSecret = parseSecret(secret); + const getAppId = (): string => appId ?? ''; const getDetail = (key: string): string => details[key] ?? ''; switch (provider.key) { case OAuthProvider.Amazon: return projectSdk.updateOAuth2Amazon({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Apple: return projectSdk.updateOAuth2Apple({ - serviceId: appId || undefined, + serviceId: getAppId(), keyId: getDetail('keyId'), teamId: getDetail('teamId'), p8File: parsedSecret.p8File || undefined, @@ -62,7 +63,7 @@ async function updateProjectOAuth({ }); case OAuthProvider.Auth0: return projectSdk.updateOAuth2Auth0({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: parsedSecret.clientSecret || undefined, endpoint: getDetail('endpoint'), enabled @@ -70,99 +71,99 @@ async function updateProjectOAuth({ case OAuthProvider.Authentik: return projectSdk.updateOAuth2Authentik({ endpoint: getDetail('endpoint'), - clientId: appId || undefined, + clientId: getAppId(), clientSecret: parsedSecret.clientSecret || undefined, enabled }); case OAuthProvider.Autodesk: return projectSdk.updateOAuth2Autodesk({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Bitbucket: return projectSdk.updateOAuth2Bitbucket({ - key: appId || undefined, + key: getAppId(), secret: secret || undefined, enabled }); case OAuthProvider.Bitly: return projectSdk.updateOAuth2Bitly({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Box: return projectSdk.updateOAuth2Box({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Dailymotion: return projectSdk.updateOAuth2Dailymotion({ - apiKey: appId || undefined, + apiKey: getAppId(), apiSecret: secret || undefined, enabled }); case OAuthProvider.Discord: return projectSdk.updateOAuth2Discord({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Disqus: return projectSdk.updateOAuth2Disqus({ - publicKey: appId || undefined, + publicKey: getAppId(), secretKey: secret || undefined, enabled }); case OAuthProvider.Dropbox: return projectSdk.updateOAuth2Dropbox({ - appKey: appId || undefined, + appKey: getAppId(), appSecret: secret || undefined, enabled }); case OAuthProvider.Etsy: return projectSdk.updateOAuth2Etsy({ - keyString: appId || undefined, + keyString: getAppId(), sharedSecret: secret || undefined, enabled }); case OAuthProvider.Facebook: return projectSdk.updateOAuth2Facebook({ - appId: appId || undefined, + appId: getAppId(), appSecret: secret || undefined, enabled }); case OAuthProvider.Figma: return projectSdk.updateOAuth2Figma({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Fusionauth: return projectSdk.updateOAuth2FusionAuth({ endpoint: getDetail('endpoint'), - clientId: appId || undefined, + clientId: getAppId(), clientSecret: parsedSecret.clientSecret || undefined, enabled }); case OAuthProvider.Github: return projectSdk.updateOAuth2GitHub({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Gitlab: return projectSdk.updateOAuth2Gitlab({ - applicationId: appId || undefined, + applicationId: getAppId(), secret: parsedSecret.secret || undefined, endpoint: getDetail('endpoint'), enabled }); case OAuthProvider.Google: return projectSdk.updateOAuth2Google({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); @@ -170,38 +171,38 @@ async function updateProjectOAuth({ return projectSdk.updateOAuth2Keycloak({ endpoint: getDetail('endpoint'), realmName: getDetail('realmName'), - clientId: appId || undefined, + clientId: getAppId(), clientSecret: parsedSecret.clientSecret || undefined, enabled }); case OAuthProvider.Kick: return projectSdk.updateOAuth2Kick({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Linkedin: return projectSdk.updateOAuth2Linkedin({ - clientId: appId || undefined, + clientId: getAppId(), primaryClientSecret: secret || undefined, enabled }); case OAuthProvider.Microsoft: return projectSdk.updateOAuth2Microsoft({ tenant: getDetail('tenant'), - applicationId: appId || undefined, + applicationId: getAppId(), applicationSecret: parsedSecret.applicationSecret || undefined, enabled }); case OAuthProvider.Notion: return projectSdk.updateOAuth2Notion({ - oauthClientId: appId || undefined, + oauthClientId: getAppId(), oauthClientSecret: secret || undefined, enabled }); case OAuthProvider.Oidc: return projectSdk.updateOAuth2Oidc({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: parsedSecret.clientSecret || undefined, wellKnownURL: getDetail('wellKnownURL'), authorizationURL: getDetail('authorizationURL'), @@ -211,7 +212,7 @@ async function updateProjectOAuth({ }); case OAuthProvider.Okta: return projectSdk.updateOAuth2Okta({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: parsedSecret.clientSecret || undefined, domain: getDetail('domain'), authorizationServerId: getDetail('authorizationServerId'), @@ -219,97 +220,97 @@ async function updateProjectOAuth({ }); case OAuthProvider.Paypal: return projectSdk.updateOAuth2Paypal({ - clientId: appId || undefined, + clientId: getAppId(), secretKey: secret || undefined, enabled }); case OAuthProvider.PaypalSandbox: return projectSdk.updateOAuth2PaypalSandbox({ - clientId: appId || undefined, + clientId: getAppId(), secretKey: secret || undefined, enabled }); case OAuthProvider.Podio: return projectSdk.updateOAuth2Podio({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Salesforce: return projectSdk.updateOAuth2Salesforce({ - customerKey: appId || undefined, + customerKey: getAppId(), customerSecret: secret || undefined, enabled }); case OAuthProvider.Slack: return projectSdk.updateOAuth2Slack({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Spotify: return projectSdk.updateOAuth2Spotify({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Stripe: return projectSdk.updateOAuth2Stripe({ - clientId: appId || undefined, + clientId: getAppId(), apiSecretKey: secret || undefined, enabled }); case OAuthProvider.Tradeshift: return projectSdk.updateOAuth2Tradeshift({ - oauth2ClientId: appId || undefined, + oauth2ClientId: getAppId(), oauth2ClientSecret: secret || undefined, enabled }); case OAuthProvider.TradeshiftBox: return projectSdk.updateOAuth2TradeshiftSandbox({ - oauth2ClientId: appId || undefined, + oauth2ClientId: getAppId(), oauth2ClientSecret: secret || undefined, enabled }); case OAuthProvider.Twitch: return projectSdk.updateOAuth2Twitch({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Wordpress: return projectSdk.updateOAuth2WordPress({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.X: return projectSdk.updateOAuth2X({ - customerKey: appId || undefined, + customerKey: getAppId(), secretKey: secret || undefined, enabled }); case OAuthProvider.Yahoo: return projectSdk.updateOAuth2Yahoo({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Yandex: return projectSdk.updateOAuth2Yandex({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Zoho: return projectSdk.updateOAuth2Zoho({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); case OAuthProvider.Zoom: return projectSdk.updateOAuth2Zoom({ - clientId: appId || undefined, + clientId: getAppId(), clientSecret: secret || undefined, enabled }); From 02b1d5e4fc5ab60777f1ea6de986700f56d0573f Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 16:00:32 +0530 Subject: [PATCH 15/27] fix lint --- .../auth/(providers)/mainOAuth.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index d398cb523..a10147428 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -61,7 +61,9 @@ return value !== null && value !== ''; }); $: hasDetailChanges = detailParams.some((param) => { - return normalizeFieldValue(fieldValues[param.$id]) !== (initialDetailValues[param.$id] ?? ''); + return ( + normalizeFieldValue(fieldValues[param.$id]) !== (initialDetailValues[param.$id] ?? '') + ); }); $: nothingChanged = enabled === initialEnabled && @@ -86,8 +88,7 @@ additionalParams.map((param) => [param.$id, getInitialFieldValue(param.$id)]) ); p8PasteMode = {}; - showSecretInput = - !appId || (provider.key === OAuthProvider.Apple && !providerKeyId); + showSecretInput = !appId || (provider.key === OAuthProvider.Apple && !providerKeyId); error = undefined; } From 84ad093201735fbfdb062974f054b21a125c097b Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 16:04:46 +0530 Subject: [PATCH 16/27] fix-11993-normalize-oauth-secret-payloads --- .../auth/(providers)/mainOAuth.svelte | 1 - .../auth/updateOAuth.ts | 96 +++++++++++-------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index a10147428..4c826b647 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -171,7 +171,6 @@ }); if (entries.length === 0) return null; - if (secretParams.length === 1) return entries[0]?.[1] ?? null; return JSON.stringify(Object.fromEntries(entries)); } diff --git a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts index 6bdb87a4b..0c2195502 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts @@ -45,12 +45,24 @@ async function updateProjectOAuth({ 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: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Apple: @@ -58,13 +70,13 @@ async function updateProjectOAuth({ serviceId: getAppId(), keyId: getDetail('keyId'), teamId: getDetail('teamId'), - p8File: parsedSecret.p8File || undefined, + p8File: getSecret('p8File'), enabled }); case OAuthProvider.Auth0: return projectSdk.updateOAuth2Auth0({ clientId: getAppId(), - clientSecret: parsedSecret.clientSecret || undefined, + clientSecret: getSecret('clientSecret'), endpoint: getDetail('endpoint'), enabled }); @@ -72,99 +84,99 @@ async function updateProjectOAuth({ return projectSdk.updateOAuth2Authentik({ endpoint: getDetail('endpoint'), clientId: getAppId(), - clientSecret: parsedSecret.clientSecret || undefined, + clientSecret: getSecret('clientSecret'), enabled }); case OAuthProvider.Autodesk: return projectSdk.updateOAuth2Autodesk({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Bitbucket: return projectSdk.updateOAuth2Bitbucket({ key: getAppId(), - secret: secret || undefined, + secret: getSecret(), enabled }); case OAuthProvider.Bitly: return projectSdk.updateOAuth2Bitly({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Box: return projectSdk.updateOAuth2Box({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Dailymotion: return projectSdk.updateOAuth2Dailymotion({ apiKey: getAppId(), - apiSecret: secret || undefined, + apiSecret: getSecret(), enabled }); case OAuthProvider.Discord: return projectSdk.updateOAuth2Discord({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Disqus: return projectSdk.updateOAuth2Disqus({ publicKey: getAppId(), - secretKey: secret || undefined, + secretKey: getSecret(), enabled }); case OAuthProvider.Dropbox: return projectSdk.updateOAuth2Dropbox({ appKey: getAppId(), - appSecret: secret || undefined, + appSecret: getSecret(), enabled }); case OAuthProvider.Etsy: return projectSdk.updateOAuth2Etsy({ keyString: getAppId(), - sharedSecret: secret || undefined, + sharedSecret: getSecret(), enabled }); case OAuthProvider.Facebook: return projectSdk.updateOAuth2Facebook({ appId: getAppId(), - appSecret: secret || undefined, + appSecret: getSecret(), enabled }); case OAuthProvider.Figma: return projectSdk.updateOAuth2Figma({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Fusionauth: return projectSdk.updateOAuth2FusionAuth({ endpoint: getDetail('endpoint'), clientId: getAppId(), - clientSecret: parsedSecret.clientSecret || undefined, + clientSecret: getSecret('clientSecret'), enabled }); case OAuthProvider.Github: return projectSdk.updateOAuth2GitHub({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Gitlab: return projectSdk.updateOAuth2Gitlab({ applicationId: getAppId(), - secret: parsedSecret.secret || undefined, + secret: getSecret('secret'), endpoint: getDetail('endpoint'), enabled }); case OAuthProvider.Google: return projectSdk.updateOAuth2Google({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Keycloak: @@ -172,38 +184,38 @@ async function updateProjectOAuth({ endpoint: getDetail('endpoint'), realmName: getDetail('realmName'), clientId: getAppId(), - clientSecret: parsedSecret.clientSecret || undefined, + clientSecret: getSecret('clientSecret'), enabled }); case OAuthProvider.Kick: return projectSdk.updateOAuth2Kick({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Linkedin: return projectSdk.updateOAuth2Linkedin({ clientId: getAppId(), - primaryClientSecret: secret || undefined, + primaryClientSecret: getSecret(), enabled }); case OAuthProvider.Microsoft: return projectSdk.updateOAuth2Microsoft({ tenant: getDetail('tenant'), applicationId: getAppId(), - applicationSecret: parsedSecret.applicationSecret || undefined, + applicationSecret: getSecret('applicationSecret'), enabled }); case OAuthProvider.Notion: return projectSdk.updateOAuth2Notion({ oauthClientId: getAppId(), - oauthClientSecret: secret || undefined, + oauthClientSecret: getSecret(), enabled }); case OAuthProvider.Oidc: return projectSdk.updateOAuth2Oidc({ clientId: getAppId(), - clientSecret: parsedSecret.clientSecret || undefined, + clientSecret: getSecret('clientSecret'), wellKnownURL: getDetail('wellKnownURL'), authorizationURL: getDetail('authorizationURL'), tokenUrl: getDetail('tokenUrl'), @@ -213,7 +225,7 @@ async function updateProjectOAuth({ case OAuthProvider.Okta: return projectSdk.updateOAuth2Okta({ clientId: getAppId(), - clientSecret: parsedSecret.clientSecret || undefined, + clientSecret: getSecret('clientSecret'), domain: getDetail('domain'), authorizationServerId: getDetail('authorizationServerId'), enabled @@ -221,97 +233,97 @@ async function updateProjectOAuth({ case OAuthProvider.Paypal: return projectSdk.updateOAuth2Paypal({ clientId: getAppId(), - secretKey: secret || undefined, + secretKey: getSecret(), enabled }); case OAuthProvider.PaypalSandbox: return projectSdk.updateOAuth2PaypalSandbox({ clientId: getAppId(), - secretKey: secret || undefined, + secretKey: getSecret(), enabled }); case OAuthProvider.Podio: return projectSdk.updateOAuth2Podio({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Salesforce: return projectSdk.updateOAuth2Salesforce({ customerKey: getAppId(), - customerSecret: secret || undefined, + customerSecret: getSecret(), enabled }); case OAuthProvider.Slack: return projectSdk.updateOAuth2Slack({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Spotify: return projectSdk.updateOAuth2Spotify({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Stripe: return projectSdk.updateOAuth2Stripe({ clientId: getAppId(), - apiSecretKey: secret || undefined, + apiSecretKey: getSecret(), enabled }); case OAuthProvider.Tradeshift: return projectSdk.updateOAuth2Tradeshift({ oauth2ClientId: getAppId(), - oauth2ClientSecret: secret || undefined, + oauth2ClientSecret: getSecret(), enabled }); case OAuthProvider.TradeshiftBox: return projectSdk.updateOAuth2TradeshiftSandbox({ oauth2ClientId: getAppId(), - oauth2ClientSecret: secret || undefined, + oauth2ClientSecret: getSecret(), enabled }); case OAuthProvider.Twitch: return projectSdk.updateOAuth2Twitch({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Wordpress: return projectSdk.updateOAuth2WordPress({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.X: return projectSdk.updateOAuth2X({ customerKey: getAppId(), - secretKey: secret || undefined, + secretKey: getSecret(), enabled }); case OAuthProvider.Yahoo: return projectSdk.updateOAuth2Yahoo({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Yandex: return projectSdk.updateOAuth2Yandex({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Zoho: return projectSdk.updateOAuth2Zoho({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); case OAuthProvider.Zoom: return projectSdk.updateOAuth2Zoom({ clientId: getAppId(), - clientSecret: secret || undefined, + clientSecret: getSecret(), enabled }); default: From d3b5416c0d50d362d886c661e2aadce193e07090 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 12:09:07 +0000 Subject: [PATCH 17/27] feat: combine bandwidth and realtime bandwidth into a single row with breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges the standalone "Realtime bandwidth" row into the "Bandwidth" row. The progress bar now renders two color-coded segments — regular bandwidth and realtime bandwidth — with hover tooltips showing the realtime value. --- .../billing/planSummary.svelte | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 84683f8c6..5f4e08b1d 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -148,6 +148,50 @@ ]; } + function createBandwidthProgressData( + regularBytes: 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 total = regularBytes + realtimeBytes; + const percentage = Math.min((total / maxBytes) * 100, 100); + const regularSize = humanFileSize(regularBytes); + const realtimeSize = humanFileSize(realtimeBytes); + + const segments: Array<{ + size: number; + color: string; + tooltip?: { title: string; label: string }; + }> = []; + + if (regularBytes > 0) { + segments.push({ + size: regularBytes, + color: 'var(--bgcolor-neutral-invert)', + tooltip: { + title: `${percentage.toFixed(0)}% used`, + label: `Bandwidth: ${regularSize.value} ${regularSize.unit}` + } + }); + } + + if (realtimeBytes > 0) { + segments.push({ + size: realtimeBytes, + color: 'var(--bgcolor-neutral-invert-weak)', + tooltip: { + title: 'Realtime bandwidth', + label: `${realtimeSize.value} ${realtimeSize.unit}` + } + }); + } + + return segments; + } + function getResource(resources: Array | undefined, resourceId: string) { return resources?.find((resource) => resource.resourceId === resourceId); } @@ -282,9 +326,16 @@ 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; + const combinedBandwidthValue = bandwidthValue + realtimeBandwidthValue; + const combinedBandwidthAmount = + (bandwidth?.amount || 0) + (realtimeBandwidth?.amount || 0); + return { id: `project-${projectData.$id}`, expandable: true, @@ -302,14 +353,20 @@ label: 'Bandwidth', resource: bandwidth, planLimit: currentPlan?.bandwidth, - usageFormatter: ({ value, planLimit, hasLimit }) => + usageFormatter: ({ planLimit, hasLimit }) => formatBandwidthUsage( - value, + combinedBandwidthValue, hasLimit ? (planLimit ?? undefined) : undefined ), - priceFormatter: ({ amount }) => formatCurrency(amount), - progressFactory: ({ value, planLimit, hasLimit }) => - hasLimit ? createStorageProgressData(value, planLimit || 0) : [], + priceFormatter: () => formatCurrency(combinedBandwidthAmount), + progressFactory: ({ planLimit, hasLimit }) => + hasLimit + ? createBandwidthProgressData( + bandwidthValue, + realtimeBandwidthValue, + planLimit || 0 + ) + : [], maxFactory: ({ planLimit, hasLimit }) => hasLimit ? (planLimit || 0) * 1000 * 1000 * 1000 : null }), @@ -383,15 +440,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', From 05e672b61862538240e7805ee23812a97ee8a87d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 19:13:15 +0530 Subject: [PATCH 18/27] fix: show error alert when listProjectScopes fails in scopes.svelte Previously a rejected API call left allScopesList empty and mounted stuck at false with no feedback to the user. Now catches the error, displays an inline alert, and sets mounted in finally so the component settles cleanly. --- .../overview/api-keys/scopes.svelte | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte index 9d146ddab..18d1a1acc 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte @@ -33,13 +33,14 @@ import { symmetricDifference } from '$lib/helpers/array'; import { cloudOnlyBackupScopes, type ScopeDefinition } from '$lib/constants'; import { sdk } from '$lib/stores/sdk'; - import { Accordion, Badge, Divider, Layout, Selector } from '@appwrite.io/pink-svelte'; + import { Accordion, Alert, Badge, Divider, Layout, Selector } from '@appwrite.io/pink-svelte'; import type { Scopes } from '@appwrite.io/console'; let { scopes = $bindable([]) }: { scopes: Scopes[] } = $props(); let allScopesList: ScopeDefinition[] = $state([]); let mounted = $state(false); + let loadError: string | null = $state(null); enum Category { Auth = 'Auth', @@ -92,19 +93,24 @@ }); onMount(async () => { - const result = await sdk.forConsole.console.listProjectScopes(); - allScopesList = result.scopes.map((s) => ({ - scope: s.$id, - description: s.description, - category: s.category, - deprecated: s.deprecated, - icon: '' - })); + try { + const result = await sdk.forConsole.console.listProjectScopes(); + allScopesList = result.scopes.map((s) => ({ + scope: s.$id, + description: s.description, + category: s.category, + deprecated: s.deprecated, + icon: '' + })); - for (const s of filteredScopes) { - activeScopes[s.scope] = scopes.includes(s.scope as Scopes); + for (const s of filteredScopes) { + activeScopes[s.scope] = scopes.includes(s.scope as Scopes); + } + } catch (e) { + loadError = e?.message ?? 'Failed to load available scopes.'; + } finally { + mounted = true; } - mounted = true; }); function selectAll() { @@ -152,6 +158,9 @@ + {#if loadError} + {loadError} + {/if} From 2e7e3e75451df3140056d16c5f1a4b163407d05a Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 19:35:16 +0530 Subject: [PATCH 19/27] fix: use example presence to infer required fields when provider is enabled Parameters with a non-empty example value are treated as required when the provider is enabled. Params without an example (e.g. GitLab endpoint for self-hosted) remain optional regardless of enabled state. --- .../auth/(providers)/mainOAuth.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte index 4c826b647..acc3a90c5 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte @@ -230,6 +230,7 @@ autofocus={true} placeholder={appIdParam.example || ''} helper={helperText(appIdParam.hint)} + required={enabled && !!appIdParam.example} bind:value={appId} /> {/if} @@ -239,6 +240,7 @@ label={primaryName(param.name)} placeholder={param.example || ''} helper={helperText(param.hint)} + required={enabled && !!param.example} bind:value={fieldValues[param.$id]} /> {/each} From 20d09f798499b539fa7072ac2812d0a78c5e3944 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 30 Apr 2026 19:37:00 +0530 Subject: [PATCH 20/27] fix: prevent scope data-loss when listProjectScopes fails mounted is now only set to true on successful load. On error, it stays false so the $effect never fires, activeScopes stays empty, and the parent-bound scopes prop is never overwritten with []. --- .../overview/api-keys/scopes.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte index 18d1a1acc..35b0ddf23 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte @@ -106,10 +106,12 @@ for (const s of filteredScopes) { activeScopes[s.scope] = scopes.includes(s.scope as Scopes); } + mounted = true; } catch (e) { loadError = e?.message ?? 'Failed to load available scopes.'; - } finally { - mounted = true; + // 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. } }); From 1f6d6883e034ee1d221025fe58ae4df1d827f9f4 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Sat, 2 May 2026 08:47:50 +0530 Subject: [PATCH 21/27] Fix scope picker visibility and function git settings updates --- .../(modals)/createGit.svelte | 19 ++++- .../settings/disconnectRepo.svelte | 3 +- .../settings/updateRepository.svelte | 24 +++++- .../overview/api-keys/scopes.svelte | 74 +++++++++++++++---- 4 files changed, 100 insertions(+), 20 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte index 1ea7fffbd..072387575 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte @@ -75,6 +75,20 @@ async function createDeployment() { try { if (!$func?.providerRepositoryId) { + const branchList = await sdk + .forProject(page.params.region, page.params.project) + .vcs.listRepositoryBranches({ + installationId: $installation.$id, + providerRepositoryId: selectedRepository + }); + const sorted = sortBranches(branchList.branches); + const nextBranch = + sorted.find((item) => item.name === branch)?.name ?? + sorted.find((item) => item.name === 'main' || item.name === 'master')?.name ?? + sorted[0]?.name ?? + branch ?? + 'main'; + await sdk.forProject(page.params.region, page.params.project).functions.update({ functionId: $func.$id, name: $func.name, @@ -90,7 +104,10 @@ scopes: ($func.scopes as Scopes[]) || undefined, installationId: $installation.$id || undefined, providerRepositoryId: selectedRepository || undefined, - providerBranch: branch || undefined + providerBranch: nextBranch, + providerSilentMode: $func.providerSilentMode ?? undefined, + providerRootDirectory: $func.providerRootDirectory ?? undefined, + buildSpecification: $func.buildSpecification || undefined }); } if (commit) { diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/disconnectRepo.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/disconnectRepo.svelte index 9ba31a85a..fc27ddfe9 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/disconnectRepo.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/disconnectRepo.svelte @@ -40,7 +40,8 @@ providerRepositoryId: '', providerBranch: '', providerSilentMode: true, - providerRootDirectory: '' + providerRootDirectory: '', + buildSpecification: $func.buildSpecification || undefined }); await invalidate(Dependencies.FUNCTION); dispatch('success'); diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateRepository.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateRepository.svelte index 1a2412dfe..c091ffe53 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateRepository.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateRepository.svelte @@ -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} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte index 35b0ddf23..14f7581bb 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte @@ -31,7 +31,11 @@ import { isCloud } from '$lib/system'; import { Button } from '$lib/elements/forms'; import { symmetricDifference } from '$lib/helpers/array'; - import { cloudOnlyBackupScopes, type ScopeDefinition } from '$lib/constants'; + 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'; @@ -41,27 +45,40 @@ let allScopesList: ScopeDefinition[] = $state([]); let mounted = $state(false); let loadError: string | null = $state(null); + const effectiveScopes = $derived(getEffectiveScopes(scopes)); + + const categoryAliasMap: Record = { + 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 ]; + function normalizeCategory(category: string): string { + return categoryAliasMap[category] ?? category; + } + const filteredScopes = $derived.by(() => { const databasesWriteIndex = allScopesList.findIndex((s) => s.scope === 'databases.write'); if (isCloud && databasesWriteIndex !== -1) { @@ -74,6 +91,19 @@ return allScopesList; }); + 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), @@ -95,16 +125,29 @@ onMount(async () => { try { const result = await sdk.forConsole.console.listProjectScopes(); - allScopesList = result.scopes.map((s) => ({ - scope: s.$id, - description: s.description, - category: s.category, - deprecated: s.deprecated, - icon: '' - })); + const scopesById = new Map(); + + 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] = scopes.includes(s.scope as Scopes); + activeScopes[s.scope] = effectiveScopes.includes(s.scope); } mounted = true; } catch (e) { @@ -129,8 +172,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) { @@ -142,7 +186,7 @@ return 'indeterminate'; } - function onCategoryChange(event: CustomEvent, category: Category) { + function onCategoryChange(event: CustomEvent, category: string) { const { detail } = event; if (detail === 'indeterminate') return; filteredScopes.forEach((s) => { @@ -177,7 +221,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} Date: Sat, 2 May 2026 08:56:06 +0530 Subject: [PATCH 22/27] Address PR feedback on scope categories and git branch fetch --- .../(modals)/createGit.svelte | 16 +--------------- .../overview/api-keys/scopes.svelte | 5 ++++- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte index 072387575..e9ee60e75 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/(modals)/createGit.svelte @@ -75,20 +75,6 @@ async function createDeployment() { try { if (!$func?.providerRepositoryId) { - const branchList = await sdk - .forProject(page.params.region, page.params.project) - .vcs.listRepositoryBranches({ - installationId: $installation.$id, - providerRepositoryId: selectedRepository - }); - const sorted = sortBranches(branchList.branches); - const nextBranch = - sorted.find((item) => item.name === branch)?.name ?? - sorted.find((item) => item.name === 'main' || item.name === 'master')?.name ?? - sorted[0]?.name ?? - branch ?? - 'main'; - await sdk.forProject(page.params.region, page.params.project).functions.update({ functionId: $func.$id, name: $func.name, @@ -104,7 +90,7 @@ scopes: ($func.scopes as Scopes[]) || undefined, installationId: $installation.$id || undefined, providerRepositoryId: selectedRepository || undefined, - providerBranch: nextBranch, + providerBranch: branch || undefined, providerSilentMode: $func.providerSilentMode ?? undefined, providerRootDirectory: $func.providerRootDirectory ?? undefined, buildSpecification: $func.buildSpecification || undefined diff --git a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte index 14f7581bb..7309623b5 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte @@ -84,7 +84,10 @@ if (isCloud && databasesWriteIndex !== -1) { return [ ...allScopesList.slice(0, databasesWriteIndex + 1), - ...cloudOnlyBackupScopes, + ...cloudOnlyBackupScopes.map((scope) => ({ + ...scope, + category: normalizeCategory(scope.category) + })), ...allScopesList.slice(databasesWriteIndex + 1) ]; } From 36d599c90174dad0d605f5e76b9e4ed784b7feb5 Mon Sep 17 00:00:00 2001 From: abhay-dev2901 Date: Sat, 2 May 2026 15:10:42 +0530 Subject: [PATCH 23/27] fix: correct backup storage usage unit label --- .../organization-[organization]/usage/[[invoice]]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte index 4a743e4e7..874cc23ec 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -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` } }, { From b7e558545e3f140aa6f863618ca16e88dbbe2e9c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:03:27 +0000 Subject: [PATCH 24/27] fix(billing): stop double-counting realtime bandwidth into bandwidth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend already rolls realtimeBandwidth value + amount into the bandwidth resource, so the previous commit was summing them again (value + amount), inflating both the displayed total and the priced amount, and over-driving the progress bar. Use bandwidth?.value / bandwidth?.amount directly (already include realtime). For the multi-segment progress bar, carve the realtime slice out of the total instead of summing — regular = total - realtime, realtime = realtime — so the segments add to the actual bandwidth total. Same neutral colour family as before, no multi-colour added. Clamps realtime to [0, total] in case of dirty data. --- .../billing/planSummary.svelte | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 5f4e08b1d..11e25948f 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -149,17 +149,20 @@ } function createBandwidthProgressData( - regularBytes: number, + 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 total = regularBytes + realtimeBytes; - const percentage = Math.min((total / maxBytes) * 100, 100); - const regularSize = humanFileSize(regularBytes); - const realtimeSize = humanFileSize(realtimeBytes); + const percentage = Math.min((totalBytes / maxBytes) * 100, 100); + // Backend already rolls realtimeBytes into totalBytes; carve the realtime + // slice out of the total instead of summing them. Clamp in case of dirty data. + const realtimePortion = Math.max(0, Math.min(realtimeBytes, totalBytes)); + const regularPortion = Math.max(0, totalBytes - realtimePortion); + const regularSize = humanFileSize(regularPortion); + const realtimeSize = humanFileSize(realtimePortion); const segments: Array<{ size: number; @@ -167,9 +170,9 @@ tooltip?: { title: string; label: string }; }> = []; - if (regularBytes > 0) { + if (regularPortion > 0) { segments.push({ - size: regularBytes, + size: regularPortion, color: 'var(--bgcolor-neutral-invert)', tooltip: { title: `${percentage.toFixed(0)}% used`, @@ -178,9 +181,9 @@ }); } - if (realtimeBytes > 0) { + if (realtimePortion > 0) { segments.push({ - size: realtimeBytes, + size: realtimePortion, color: 'var(--bgcolor-neutral-invert-weak)', tooltip: { title: 'Realtime bandwidth', @@ -330,11 +333,11 @@ const storage = getResource(resources, 'storage'); const authPhone = getResource(resources, 'authPhone'); + // Backend already rolls realtimeBandwidth value + amount into bandwidth; + // use the bandwidth resource directly and pass realtime only as a slice + // for the progress segment, never as an additive total. const bandwidthValue = bandwidth?.value || 0; const realtimeBandwidthValue = realtimeBandwidth?.value || 0; - const combinedBandwidthValue = bandwidthValue + realtimeBandwidthValue; - const combinedBandwidthAmount = - (bandwidth?.amount || 0) + (realtimeBandwidth?.amount || 0); return { id: `project-${projectData.$id}`, @@ -353,16 +356,16 @@ label: 'Bandwidth', resource: bandwidth, planLimit: currentPlan?.bandwidth, - usageFormatter: ({ planLimit, hasLimit }) => + usageFormatter: ({ value, planLimit, hasLimit }) => formatBandwidthUsage( - combinedBandwidthValue, + value, hasLimit ? (planLimit ?? undefined) : undefined ), - priceFormatter: () => formatCurrency(combinedBandwidthAmount), - progressFactory: ({ planLimit, hasLimit }) => + priceFormatter: ({ amount }) => formatCurrency(amount), + progressFactory: ({ value, planLimit, hasLimit }) => hasLimit ? createBandwidthProgressData( - bandwidthValue, + value, realtimeBandwidthValue, planLimit || 0 ) From 0de30dad10ecdfdc37e322cf214530ec828150c0 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:04:34 +0000 Subject: [PATCH 25/27] fix(billing): skip realtime segment when bandwidth < realtimeBandwidth A handful of legacy orgs have realtime bandwidth tracked separately (not yet billed and not added into the bandwidth resource). For them realtimeBytes > totalBytes, which would otherwise overflow the bar or collapse the regular segment to zero. Detect that case and render bandwidth as a single segment instead, preserving today's behaviour for those orgs. --- .../billing/planSummary.svelte | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 11e25948f..e611e8fe9 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -157,9 +157,12 @@ const maxBytes = maxGB * 1000 * 1000 * 1000; const percentage = Math.min((totalBytes / maxBytes) * 100, 100); - // Backend already rolls realtimeBytes into totalBytes; carve the realtime - // slice out of the total instead of summing them. Clamp in case of dirty data. - const realtimePortion = Math.max(0, Math.min(realtimeBytes, totalBytes)); + // Backend rolls realtimeBytes into totalBytes for billed orgs, so realtime + // is a slice of the total. A handful of legacy orgs still have realtime + // tracked separately (not billed yet) — for them realtime > total. In that + // case skip the breakdown and render the bandwidth as a single segment. + 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); From f4a67818447ac275f61a86f4871f9571b47c1bcc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:07:54 +0000 Subject: [PATCH 26/27] fix(billing): surface separate Realtime bandwidth row for legacy orgs A handful of orgs still have realtime bandwidth tracked outside the bandwidth resource (not billed/rolled in yet). Earlier commit hid their realtime value entirely once the breakdown bar suppressed it. Render a standalone informational Realtime bandwidth row only when realtimeBandwidthValue > bandwidthValue, matching today's pre-merge behaviour for those orgs while keeping the modern path one row. --- .../billing/planSummary.svelte | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index e611e8fe9..d48b95100 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -376,6 +376,25 @@ maxFactory: ({ planLimit, hasLimit }) => hasLimit ? (planLimit || 0) * 1000 * 1000 * 1000 : null }), + // Legacy orgs still track realtime bandwidth separately (not yet + // rolled into the bandwidth resource). Surface it as its own row + // so the value is visible even though it can't fit as a slice + // of the bandwidth bar. + ...(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', From 3d6844b3563264450ac397fe127a23aec7c7ff0e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:15:36 +0000 Subject: [PATCH 27/27] chore: drop explanatory comments per repo convention --- .../billing/planSummary.svelte | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index d48b95100..cc56a826c 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -157,10 +157,6 @@ const maxBytes = maxGB * 1000 * 1000 * 1000; const percentage = Math.min((totalBytes / maxBytes) * 100, 100); - // Backend rolls realtimeBytes into totalBytes for billed orgs, so realtime - // is a slice of the total. A handful of legacy orgs still have realtime - // tracked separately (not billed yet) — for them realtime > total. In that - // case skip the breakdown and render the bandwidth as a single segment. const showRealtimeSegment = realtimeBytes > 0 && realtimeBytes <= totalBytes; const realtimePortion = showRealtimeSegment ? realtimeBytes : 0; const regularPortion = Math.max(0, totalBytes - realtimePortion); @@ -336,9 +332,6 @@ const storage = getResource(resources, 'storage'); const authPhone = getResource(resources, 'authPhone'); - // Backend already rolls realtimeBandwidth value + amount into bandwidth; - // use the bandwidth resource directly and pass realtime only as a slice - // for the progress segment, never as an additive total. const bandwidthValue = bandwidth?.value || 0; const realtimeBandwidthValue = realtimeBandwidth?.value || 0; @@ -376,10 +369,6 @@ maxFactory: ({ planLimit, hasLimit }) => hasLimit ? (planLimit || 0) * 1000 * 1000 * 1000 : null }), - // Legacy orgs still track realtime bandwidth separately (not yet - // rolled into the bandwidth resource). Surface it as its own row - // so the value is visible even though it can't fit as a slice - // of the bandwidth bar. ...(realtimeBandwidthValue > bandwidthValue ? [ createRow({