Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@82d2831",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@1a5604f",
"@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",
Expand Down
3 changes: 3 additions & 0 deletions src/lib/actions/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ export enum Submit {
AccountRecoveryCodesCreate = 'submit_account_recovery_codes_create',
AccountRecoveryCodesUpdate = 'submit_account_recovery_codes_update',
AccountDeleteIdentity = 'submit_account_delete_identity',
AccountOAuth2ConsentApprove = 'submit_account_oauth2_consent_approve',
AccountOAuth2ConsentDeny = 'submit_account_oauth2_consent_deny',
AccountOAuth2DeviceVerify = 'submit_account_oauth2_device_verify',
FeedbackSubmit = 'submit_leave_feedback',
FilterClear = 'submit_clear_filter',
FilterApply = 'submit_filter_apply',
Expand Down
90 changes: 90 additions & 0 deletions src/lib/helpers/oauth2-scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { ComponentType } from 'svelte';
import {
IconShieldCheck,
IconUser,
IconMail,
IconIdentification,
IconKey
} from '@appwrite.io/pink-icons-svelte';

export interface ScopeDescriptor {
id: string;
title: string;
description: string;
icon: ComponentType;
}

/**
* This consent screen always authorizes against the Appwrite **console**
* project. On the server, any OAuth2 access token issued for the console
* project is granted the full `users` (member) role — the same access a
* signed-in console session has — regardless of the OIDC scopes requested.
* The `openid`/`profile`/`email` scopes only shape the OIDC identity claims;
* they do NOT limit what the application can do. So the consent screen must
* lead with the full-access reality rather than implying read-only access.
*/
export const FULL_ACCESS_SCOPE: ScopeDescriptor = {
id: '__full_access__',
title: 'Full access to your account',
description: 'Manage your organizations, projects, and all their resources on your behalf.',
icon: IconShieldCheck
};

const BUILTIN_SCOPES: Record<string, Omit<ScopeDescriptor, 'id'>> = {
openid: {
title: 'Verify your identity',
description: 'Confirm who you are using your Appwrite account.',
icon: IconIdentification
},
profile: {
title: 'View your profile',
description: 'Read your name and profile details.',
icon: IconUser
},
email: {
title: 'View your email address',
description: 'Read the email address associated with your account.',
icon: IconMail
}
};

function titleizeScope(scope: string): string {
const cleaned = scope.replace(/[._:-]+/g, ' ').trim();
if (!cleaned) return scope;
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
}

export function describeScope(scope: string): ScopeDescriptor {
const builtin = BUILTIN_SCOPES[scope];
if (builtin) {
return { id: scope, ...builtin };
}
return {
id: scope,
title: titleizeScope(scope),
description: `Access to ${scope}.`,
icon: IconKey
};
}

export function describeScopes(scopes: string[]): ScopeDescriptor[] {
return scopes.map(describeScope);
}

// Identity scopes shown (in this order) as secondary detail beneath the
// full-access item. `openid` is intentionally omitted — identity verification
// is implied by full account access, so listing it separately is redundant.
const CONSENT_IDENTITY_SCOPES = ['profile', 'email'] as const;

/**
* Build the permission list for the console OAuth2 consent screen. Always leads
* with the full-access item (the true effect of authorizing), followed by the
* identity scopes the application actually reads (profile, email) when present.
*/
export function describeConsentScopes(scopes: string[]): ScopeDescriptor[] {
const requested = new Set(scopes);
const identity = CONSENT_IDENTITY_SCOPES.filter((scope) => requested.has(scope)).map(
describeScope
);
return [FULL_ACCESS_SCOPE, ...identity];
}
4 changes: 4 additions & 0 deletions src/lib/stores/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isMultiRegionSupported, VARS } from '$lib/system';
import { registerImpersonationClients, restoreImpersonation } from '$lib/appwrite/impersonation';
import {
Account,
Apps,
Assistant,
Avatars,
Backups,
Expand All @@ -12,6 +13,7 @@ import {
Locale,
Messaging,
Migrations,
Oauth2,
Organization,
Project,
Project as ProjectApi,
Expand Down Expand Up @@ -48,6 +50,8 @@ function createConsoleSdk(client: Client) {
return {
client,
account: new Account(client),
apps: new Apps(client),
oauth2: new Oauth2(client),
avatars: new Avatars(client),
functions: new Functions(client),
health: new Health(client),
Expand Down
28 changes: 28 additions & 0 deletions src/routes/(public)/(guest)/login/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@
return;
}

// Honor the `redirect` query param so OAuth2 consent/device flows
// (and other deep links) resume after email login. MFA-safe: if
// additional factors are required, fall through to invalidate(ACCOUNT)
// so the root layout routes to /mfa carrying the redirect param from
// the current /login URL.
const redirect = page.url.searchParams.get('redirect');
if (redirect) {
try {
await sdk.forConsole.account.get();
page.url.searchParams.delete('redirect');
await goto(`${redirect}${page.url.search}`);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
await invalidate(Dependencies.ACCOUNT);
return;
} catch (mfaError) {
if (mfaError?.type !== 'user_more_factors_required') {
addNotification({
type: 'error',
message: mfaError.message
});
trackError(mfaError, Submit.AccountLogin);
disabled = false;
return;
}
// MFA required: fall through so the root layout redirects to
// /mfa with the redirect param preserved in the URL.
}
}

// no specific redirect, so redirect will happen through invalidating the account
await invalidate(Dependencies.ACCOUNT);
} catch (error) {
Expand Down
62 changes: 62 additions & 0 deletions src/routes/(public)/oauth2/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script lang="ts">
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { loading } from '$routes/store';
import { Typography } from '@appwrite.io/pink-svelte';

loading.set(false);
</script>

<div class="auth-bg">
<section>
<div class="console-container">
<slot />
</div>
</section>
<footer>
<Typography.Eyebrow color="--fgcolor-neutral-secondary">POWERED BY</Typography.Eyebrow>
{#if $app.themeInUse === 'dark'}
<img
src="{base}/images/appwrite-logo-dark.svg"
width="120"
height="22"
alt="Appwrite Logo" />
{:else}
<img
src="{base}/images/appwrite-logo-light.svg"
width="120"
height="22"
alt="Appwrite Logo" />
{/if}
</footer>
</div>

<style lang="scss">
.auth-bg {
position: fixed;
background: var(--bgcolor-neutral-primary, #fff);
background-size: cover;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
justify-content: space-between;
section {
flex: 1;
display: flex;
align-items: center;
}
footer {
padding: 2rem 1rem;
display: flex;
gap: 0.5rem;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
}
</style>
Loading
Loading