Merge pull request #3039 from appwrite/feat-signup-email-security

feat: add signup email security controls + ce refactor changes
This commit is contained in:
Harsh Mahajan
2026-05-14 10:28:28 +05:30
committed by GitHub
54 changed files with 750 additions and 7325 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
key: ${{ runner.os }}-playwright-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-playwright-
+4 -4
View File
@@ -6,7 +6,7 @@
"name": "@appwrite/console",
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "^12.2.0",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7789ae4",
"@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,15 +124,15 @@
"@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="],
"@appwrite.io/console": ["@appwrite.io/console@12.2.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-kv5CS/pWlSkLzG1mPctUr5fMdisZRrkOYStIvAaNPHv+bl/wFCZMMoRoiPGlBpw2NKwTddGFkkQvHq8GbxehNA=="],
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@7789ae4", { "dependencies": { "json-bigint": "1.0.0" } }],
"@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="],
"@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", { "peerDependencies": { "svelte": "^4.0.0" } }, "sha512-2HYl/CC2OlfZIR7LzbLXuSPBn0iNkjbnxpaeGCkZ7UNZ/hFeSeeWjDJqTBMdZ8+X95uuZqHx62XPTiE/svuSXQ=="],
"@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", { "peerDependencies": { "svelte": "^4.0.0" } }],
"@appwrite.io/pink-legacy": ["@appwrite.io/pink-legacy@1.0.3", "", { "dependencies": { "@appwrite.io/pink-icons": "1.0.0", "the-new-css-reset": "^1.11.2" } }, "sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ=="],
"@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8dcaa17", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }, "sha512-rw3zXN7/cUciCnhj0FR8M0H5Db+LYYMaKtPxvOAIMxNTBmStzU8kTw6grqIvdtFu9vybIsjKtIwm9QLHpNDBjA=="],
"@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8dcaa17", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
+1 -1
View File
@@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "^12.2.0",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7789ae4",
"@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",
-7003
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -272,6 +272,9 @@ export enum Submit {
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
AuthAliasedEmailsUpdate = 'submit_auth_aliased_emails_update',
AuthDisposableEmailsUpdate = 'submit_auth_disposable_emails_update',
AuthFreeEmailsUpdate = 'submit_auth_free_emails_update',
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
AuthMembershipPrivacyUpdate = 'submit_auth_membership_privacy_update',
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
+3 -2
View File
@@ -70,10 +70,11 @@
function getProgressCard() {
if (!activeProject || hasOnboardingDismissed(activeProject.$id, $user)) return undefined;
const { platforms, pingCount } = activeProject;
const platforms = page.data.platforms as Models.PlatformList | undefined;
const { pingCount } = activeProject;
let percentage = 33;
if (platforms.length > 0 && pingCount === 0) {
if ((platforms?.total ?? 0) > 0 && pingCount === 0) {
percentage = 66;
} else if (pingCount > 0) {
percentage = 100;
+20 -16
View File
@@ -1,48 +1,52 @@
import { writable } from 'svelte/store';
import { type Models, AuthMethod as MethodId } from '@appwrite.io/console';
import { type Models, ProjectAuthMethodId } from '@appwrite.io/console';
export type AuthMethod = {
label: string;
method: MethodId;
method: ProjectAuthMethodId;
value: boolean | null;
};
const setAuthMethod = (project: Models.Project): AuthMethod[] => {
const authMethods = new Map(
project?.authMethods?.map((method) => [method.$id, method.enabled])
);
return [
{
label: 'Email/Password',
method: MethodId.Emailpassword,
value: project?.authEmailPassword
method: ProjectAuthMethodId.Emailpassword,
value: authMethods.get(ProjectAuthMethodId.Emailpassword) ?? null
},
{
label: 'Phone',
method: MethodId.Phone,
value: project?.authPhone
method: ProjectAuthMethodId.Phone,
value: authMethods.get(ProjectAuthMethodId.Phone) ?? null
},
{
label: 'Magic URL',
method: MethodId.Magicurl,
value: project?.authUsersAuthMagicURL
method: ProjectAuthMethodId.Magicurl,
value: authMethods.get(ProjectAuthMethodId.Magicurl) ?? null
},
{
label: 'Email OTP',
method: MethodId.Emailotp,
value: project?.authEmailOtp
method: ProjectAuthMethodId.Emailotp,
value: authMethods.get(ProjectAuthMethodId.Emailotp) ?? null
},
{
label: 'Anonymous',
method: MethodId.Anonymous,
value: project?.authAnonymous
method: ProjectAuthMethodId.Anonymous,
value: authMethods.get(ProjectAuthMethodId.Anonymous) ?? null
},
{
label: 'Team Invites',
method: MethodId.Invites,
value: project?.authInvites
method: ProjectAuthMethodId.Invites,
value: authMethods.get(ProjectAuthMethodId.Invites) ?? null
},
{
label: 'JWT',
method: MethodId.Jwt,
value: project?.authJWT
method: ProjectAuthMethodId.Jwt,
value: authMethods.get(ProjectAuthMethodId.Jwt) ?? null
}
];
};
+12 -8
View File
@@ -1,28 +1,32 @@
import { writable } from 'svelte/store';
import { ProtocolId, type Models } from '@appwrite.io/console';
import { ProjectProtocolId, type Models } from '@appwrite.io/console';
export type Protocol = {
label: string;
method: ProtocolId;
method: ProjectProtocolId;
value: boolean | null;
};
function projectProtocolRows(project: Models.Project | null): Protocol[] {
const protocols = new Map(
project?.protocols?.map((protocol) => [protocol.$id, protocol.enabled])
);
return [
{
label: 'REST',
method: ProtocolId.Rest,
value: project?.protocolStatusForRest ?? null
method: ProjectProtocolId.Rest,
value: protocols.get(ProjectProtocolId.Rest) ?? null
},
{
label: 'GraphQL',
method: ProtocolId.Graphql,
value: project?.protocolStatusForGraphql ?? null
method: ProjectProtocolId.Graphql,
value: protocols.get(ProjectProtocolId.Graphql) ?? null
},
{
label: 'WebSocket',
method: ProtocolId.Websocket,
value: project?.protocolStatusForWebsocket ?? null
method: ProjectProtocolId.Websocket,
value: protocols.get(ProjectProtocolId.Websocket) ?? null
}
];
}
+32 -30
View File
@@ -1,89 +1,91 @@
import { writable } from 'svelte/store';
import { ServiceId, type Models } from '@appwrite.io/console';
import { ProjectServiceId, type Models } from '@appwrite.io/console';
export type Service = {
label: string;
method: ServiceId;
method: ProjectServiceId;
value: boolean | null;
};
function projectServiceRows(project: Models.Project | null): Service[] {
const services = new Map(project?.services?.map((service) => [service.$id, service.enabled]));
const rows: Service[] = [
{
label: 'Account',
method: ServiceId.Account,
value: project?.serviceStatusForAccount ?? null
method: ProjectServiceId.Account,
value: services.get(ProjectServiceId.Account) ?? null
},
{
label: 'Avatars',
method: ServiceId.Avatars,
value: project?.serviceStatusForAvatars ?? null
method: ProjectServiceId.Avatars,
value: services.get(ProjectServiceId.Avatars) ?? null
},
{
label: 'Databases',
method: ServiceId.Databases,
value: project?.serviceStatusForDatabases ?? null
method: ProjectServiceId.Databases,
value: services.get(ProjectServiceId.Databases) ?? null
},
{
label: 'Functions',
method: ServiceId.Functions,
value: project?.serviceStatusForFunctions ?? null
method: ProjectServiceId.Functions,
value: services.get(ProjectServiceId.Functions) ?? null
},
{
label: 'Locale',
method: ServiceId.Locale,
value: project?.serviceStatusForLocale ?? null
method: ProjectServiceId.Locale,
value: services.get(ProjectServiceId.Locale) ?? null
},
{
label: 'Messaging',
method: ServiceId.Messaging,
value: project?.serviceStatusForMessaging ?? null
method: ProjectServiceId.Messaging,
value: services.get(ProjectServiceId.Messaging) ?? null
},
{
label: 'Migrations',
method: ServiceId.Migrations,
value: project?.serviceStatusForMigrations ?? null
method: ProjectServiceId.Migrations,
value: services.get(ProjectServiceId.Migrations) ?? null
},
{
label: 'Project',
method: ServiceId.Project,
value: project?.serviceStatusForProject ?? null
method: ProjectServiceId.Project,
value: services.get(ProjectServiceId.Project) ?? null
},
// @todo Re-enable when Proxy is ready for public release.
// {
// label: 'Proxy',
// method: ServiceId.Proxy,
// method: ProjectServiceId.Proxy,
// value: project?.serviceStatusForProxy ?? null
// },
{
label: 'Sites',
method: ServiceId.Sites,
value: project?.serviceStatusForSites ?? null
method: ProjectServiceId.Sites,
value: services.get(ProjectServiceId.Sites) ?? null
},
{
label: 'Storage',
method: ServiceId.Storage,
value: project?.serviceStatusForStorage ?? null
method: ProjectServiceId.Storage,
value: services.get(ProjectServiceId.Storage) ?? null
},
{
label: 'TablesDB',
method: ServiceId.Tablesdb,
value: project?.serviceStatusForTablesdb ?? null
method: ProjectServiceId.Tablesdb,
value: services.get(ProjectServiceId.Tablesdb) ?? null
},
{
label: 'Teams',
method: ServiceId.Teams,
value: project?.serviceStatusForTeams ?? null
method: ProjectServiceId.Teams,
value: services.get(ProjectServiceId.Teams) ?? null
},
{
label: 'Users',
method: ServiceId.Users,
value: project?.serviceStatusForUsers ?? null
method: ProjectServiceId.Users,
value: services.get(ProjectServiceId.Users) ?? null
}
// @todo Re-enable when VCS is ready for public release.
// {
// label: 'VCS',
// method: ServiceId.Vcs,
// method: ProjectServiceId.Vcs,
// value: project?.serviceStatusForVcs ?? null
// }
];
@@ -1,5 +1,6 @@
<script lang="ts">
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import CreateProject from './createProject.svelte';
@@ -51,6 +52,7 @@
let freePlanAlertDismissed = $state(false);
let searchQuery: SearchQuery | null = $state(null);
let handledCreateProjectQuery = $state(false);
const educationProgramId = 'github-student-developer';
const isEducationProgram = $derived(data.program?.$id === educationProgramId);
@@ -84,11 +86,26 @@
}
function handleCreateProject() {
if (!$canWriteProjects) return;
if (projectCreationDisabled) return;
if (isCloud) showCreateProjectCloud = true;
else showCreate = true;
}
$effect(() => {
if (handledCreateProjectQuery || !page.url.searchParams.has('create-project')) return;
handledCreateProjectQuery = true;
handleCreateProject();
const url = new URL(page.url);
url.searchParams.delete('create-project');
void goto(`${url.pathname}${url.search}${url.hash}`, {
replaceState: true,
noScroll: true,
keepFocus: true
});
});
function getIconForPlatform(platform: string): ComponentType {
switch (platform) {
case 'code':
@@ -233,13 +250,15 @@
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
{@const projectPlatforms = project.platforms}
{@const platformsTotal = project.platformsTotal}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
projectPlatforms.map((platform) => getPlatformInfo(platform.type))
)}
<GridItem1
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
{platformsTotal ? platformsTotal : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
@@ -264,10 +283,10 @@
</Badge>
{/each}
{#if platforms.length > 3}
{#if platformsTotal > 2}
<Badge
variant="secondary"
content={`+${platforms.length - 2}`}
content={`+${platformsTotal - 2}`}
style="width: max-content;" />
{/if}
@@ -43,15 +43,28 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
]
});
// set `default` if no region!
for (const project of activeProjects.projects) {
project.region ??= 'default';
}
const projects = await Promise.all(
activeProjects.projects.map(async (project) => {
project.region ??= 'default';
const platformList = await sdk
.forProject(project.region, project.$id)
.project.listPlatforms({ queries: [Query.limit(3)] });
return {
...project,
platforms: platformList.platforms,
platformsTotal: platformList.total
};
})
);
return {
limit,
offset,
search,
projects: activeProjects
projects: {
...activeProjects,
projects
}
};
};
@@ -8,7 +8,7 @@ import { get } from 'svelte/store';
import { headerAlert } from '$lib/stores/headerAlert';
import PaymentFailed from '$lib/components/billing/alerts/paymentFailed.svelte';
import { loadAvailableRegions } from '$routes/(console)/regions';
import { type Models, Platform } from '@appwrite.io/console';
import { Query, type Models, Platform } from '@appwrite.io/console';
import { redirect } from '@sveltejs/kit';
import { resolve } from '$app/paths';
import { generateFingerprintToken } from '$lib/helpers/fingerprint';
@@ -19,7 +19,8 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
const { plansInfo, organizations, preferences: prefs } = await parent();
depends(Dependencies.PROJECT);
const project = await sdk.forConsole.projects.get({ projectId: params.project });
const projectSdk = sdk.forProject(params.region, params.project).project;
const project = await projectSdk.get();
if (project.status !== 'active' && project.status !== 'paused') {
// project isn't active, redirect back to organizations page
redirect(
@@ -40,20 +41,24 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
// organization can be null if not in the filtered list!
const includedInBasePlans = plansInfo.has(organization?.billingPlanId);
const [org, rawRegionalConsoleVariables, rolesResult] = await Promise.all([
!organization
? // TODO: @itznotabug - teams.get with Models.Organization?
(sdk.forConsole.teams.get({ teamId: project.teamId }) as Promise<Models.Organization>)
: organization,
sdk.forConsoleIn(project.region).console.variables(),
isCloud
? sdk.forConsole.organizations.getScopes({
organizationId: project.teamId
})
: null,
loadAvailableRegions(project.teamId)
]);
const [org, rawRegionalConsoleVariables, rolesResult, platformList, keyList] =
await Promise.all([
!organization
? // TODO: @itznotabug - teams.get with Models.Organization?
(sdk.forConsole.teams.get({
teamId: project.teamId
}) as Promise<Models.Organization>)
: organization,
sdk.forConsoleIn(project.region).console.variables(),
isCloud
? sdk.forConsole.organizations.getScopes({
organizationId: project.teamId
})
: null,
projectSdk.listPlatforms({ queries: [Query.limit(1)] }),
projectSdk.listKeys({ queries: [Query.limit(1)] }),
loadAvailableRegions(project.teamId)
]);
const regionalConsoleVariables = normalizeConsoleVariables(rawRegionalConsoleVariables);
@@ -122,6 +127,8 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
return {
project,
platforms: platformList,
keys: keyList,
organization,
regionalConsoleVariables,
roles,
@@ -9,7 +9,8 @@
InputTextarea
} from '$lib/elements/forms';
import { updateOAuth } from '../updateOAuth';
import { OAuthProvider, type Models, type Models as ConsoleModels } from '@appwrite.io/console';
import { OAuthProvider, type Models as ConsoleModels } from '@appwrite.io/console';
import type { AuthProvider } from '../updateOAuth';
import { oAuthProviders } from '$lib/stores/oauth-providers';
import {
Accordion,
@@ -28,7 +29,7 @@
const projectId = page.params.project;
const region = page.params.region;
export let provider: Models.AuthProvider;
export let provider: AuthProvider;
export let show = false;
export let parameters: ConsoleModels.ConsoleOAuth2ProviderParameter[] = [];
@@ -39,7 +40,7 @@
let showSecretInput = false;
let p8PasteMode: Record<string, boolean> = {};
let error: string;
let initializedProvider: Models.AuthProvider | null = null;
let initializedProvider: AuthProvider | null = null;
let initialEnabled = false;
let initialAppId: string | null = null;
let initialDetailValues: Record<string, string> = {};
@@ -8,16 +8,29 @@
import UpdateSessionsLimit from './updateSessionsLimit.svelte';
import PasswordPolicies from './passwordPolicies.svelte';
import SessionSecurity from './sessionSecurity.svelte';
import UpdateSignupEmailSecurity from './updateSignupEmailSecurity.svelte';
let { data }: PageProps = $props();
</script>
<Container>
<UpdateUsersLimit project={data.project} />
<UpdateSessionLength project={data.project} />
<UpdateSessionsLimit project={data.project} />
<PasswordPolicies project={data.project} />
<SessionSecurity project={data.project} />
<UpdateUsersLimit project={data.project} policy={data.userLimitPolicy} />
<UpdateSessionLength project={data.project} policy={data.sessionDurationPolicy} />
<UpdateSessionsLimit project={data.project} policy={data.sessionLimitPolicy} />
<PasswordPolicies
project={data.project}
dictionaryPolicy={data.passwordDictionaryPolicy}
historyPolicy={data.passwordHistoryPolicy}
personalDataPolicy={data.passwordPersonalDataPolicy} />
<UpdateSignupEmailSecurity
project={data.project}
denyAliasedEmailPolicy={data.denyAliasedEmailPolicy}
denyDisposableEmailPolicy={data.denyDisposableEmailPolicy}
denyFreeEmailPolicy={data.denyFreeEmailPolicy} />
<SessionSecurity
project={data.project}
sessionAlertPolicy={data.sessionAlertPolicy}
sessionInvalidationPolicy={data.sessionInvalidationPolicy} />
<UpdateMockNumbers project={data.project} />
<UpdateMembershipPrivacy project={data.project} />
<UpdateMembershipPrivacy project={data.project} policy={data.membershipPrivacyPolicy} />
</Container>
@@ -0,0 +1,65 @@
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { ProjectPolicyId, type Models } from '@appwrite.io/console';
import type { PageLoad } from './$types';
type EnabledPolicy = {
$id: string;
enabled: boolean;
};
const ProjectEmailPolicyId = {
DenyAliasedEmail: 'deny-aliased-email',
DenyDisposableEmail: 'deny-disposable-email',
DenyFreeEmail: 'deny-free-email'
} as const;
type ProjectPolicy = Models.PolicyList['policies'][number] | EnabledPolicy;
type EmailPolicyId = (typeof ProjectEmailPolicyId)[keyof typeof ProjectEmailPolicyId];
const getDefaultEnabledPolicy = (policyId: EmailPolicyId): EnabledPolicy => ({
$id: policyId,
enabled: false
});
export const load: PageLoad = async ({ depends, params }) => {
depends(Dependencies.PROJECT);
const { policies } = await sdk.forProject(params.region, params.project).project.listPolicies();
const policiesById = Object.fromEntries(
(policies as ProjectPolicy[]).map((policy) => [policy.$id, policy])
) as Partial<Record<ProjectPolicyId | EmailPolicyId, ProjectPolicy>>;
return {
membershipPrivacyPolicy: policiesById[
ProjectPolicyId.Membershipprivacy
] as Models.PolicyMembershipPrivacy,
passwordDictionaryPolicy: policiesById[
ProjectPolicyId.Passworddictionary
] as Models.PolicyPasswordDictionary,
passwordHistoryPolicy: policiesById[
ProjectPolicyId.Passwordhistory
] as Models.PolicyPasswordHistory,
passwordPersonalDataPolicy: policiesById[
ProjectPolicyId.Passwordpersonaldata
] as Models.PolicyPasswordPersonalData,
sessionAlertPolicy: policiesById[ProjectPolicyId.Sessionalert] as Models.PolicySessionAlert,
sessionDurationPolicy: policiesById[
ProjectPolicyId.Sessionduration
] as Models.PolicySessionDuration,
sessionInvalidationPolicy: policiesById[
ProjectPolicyId.Sessioninvalidation
] as Models.PolicySessionInvalidation,
sessionLimitPolicy: policiesById[ProjectPolicyId.Sessionlimit] as Models.PolicySessionLimit,
userLimitPolicy: policiesById[ProjectPolicyId.Userlimit] as Models.PolicyUserLimit,
denyAliasedEmailPolicy:
(policiesById[ProjectEmailPolicyId.DenyAliasedEmail] as EnabledPolicy) ??
getDefaultEnabledPolicy(ProjectEmailPolicyId.DenyAliasedEmail),
denyDisposableEmailPolicy:
(policiesById[ProjectEmailPolicyId.DenyDisposableEmail] as EnabledPolicy) ??
getDefaultEnabledPolicy(ProjectEmailPolicyId.DenyDisposableEmail),
denyFreeEmailPolicy:
(policiesById[ProjectEmailPolicyId.DenyFreeEmail] as EnabledPolicy) ??
getDefaultEnabledPolicy(ProjectEmailPolicyId.DenyFreeEmail)
};
};
@@ -11,9 +11,15 @@
import { onMount } from 'svelte';
let {
project
project,
dictionaryPolicy,
historyPolicy,
personalDataPolicy
}: {
project: Models.Project;
dictionaryPolicy: Models.PolicyPasswordDictionary;
historyPolicy: Models.PolicyPasswordHistory;
personalDataPolicy: Models.PolicyPasswordPersonalData;
} = $props();
let lastValidLimit = $state(5);
@@ -24,15 +30,15 @@
onMount(() => {
// update initial states here in onMount.
const historyValue = project.authPasswordHistory;
const historyValue = historyPolicy.total;
if (historyValue && historyValue > 0) {
passwordHistory = historyValue;
lastValidLimit = historyValue;
}
passwordHistoryEnabled = (historyValue ?? 0) !== 0;
passwordDictionary = project.authPasswordDictionary ?? false;
authPersonalDataCheck = project.authPersonalDataCheck ?? false;
passwordDictionary = dictionaryPolicy.enabled;
authPersonalDataCheck = personalDataPolicy.enabled;
});
$effect(() => {
@@ -43,13 +49,11 @@
});
const hasChanges = $derived.by(() => {
const dictChanged = passwordDictionary !== (project.authPasswordDictionary ?? false);
const dataCheckChanged = authPersonalDataCheck !== (project.authPersonalDataCheck ?? false);
const historyChanged =
passwordHistoryEnabled !== ((project.authPasswordHistory ?? 0) !== 0);
const dictChanged = passwordDictionary !== dictionaryPolicy.enabled;
const dataCheckChanged = authPersonalDataCheck !== personalDataPolicy.enabled;
const historyChanged = passwordHistoryEnabled !== (historyPolicy.total !== 0);
const limitChanged =
passwordHistoryEnabled &&
Number(passwordHistory) !== (project.authPasswordHistory ?? 0);
passwordHistoryEnabled && Number(passwordHistory) !== historyPolicy.total;
return historyChanged || dictChanged || dataCheckChanged || limitChanged;
});
@@ -10,20 +10,27 @@
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
let { project }: { project: Models.Project } = $props();
let {
project,
sessionAlertPolicy,
sessionInvalidationPolicy
}: {
project: Models.Project;
sessionAlertPolicy: Models.PolicySessionAlert;
sessionInvalidationPolicy: Models.PolicySessionInvalidation;
} = $props();
let authSessionAlerts = $state(false);
let sessionInvalidation = $state(false);
onMount(() => {
authSessionAlerts = project?.authSessionAlerts ?? false;
sessionInvalidation = project?.authInvalidateSessions ?? false;
authSessionAlerts = sessionAlertPolicy.enabled;
sessionInvalidation = sessionInvalidationPolicy.enabled;
});
const hasChanges = $derived.by(() => {
const alertsChanged = authSessionAlerts !== (project?.authSessionAlerts ?? false);
const invalidationChanged =
sessionInvalidation !== (project?.authInvalidateSessions ?? false);
const alertsChanged = authSessionAlerts !== sessionAlertPolicy.enabled;
const invalidationChanged = sessionInvalidation !== sessionInvalidationPolicy.enabled;
return alertsChanged || invalidationChanged;
});
@@ -10,23 +10,25 @@
import type { Models } from '@appwrite.io/console';
const {
project
project,
policy
}: {
project: Models.Project;
policy: Models.PolicyMembershipPrivacy;
} = $props();
let authMembershipsMfa = $state(project?.authMembershipsMfa ?? true);
let authMembershipsUserId = $state(project?.authMembershipsUserId ?? true);
let authMembershipsUserName = $state(project?.authMembershipsUserName ?? true);
let authMembershipsUserEmail = $state(project?.authMembershipsUserEmail ?? true);
let authMembershipsUserPhone = $state(project?.authMembershipsUserPhone ?? true);
let authMembershipsMfa = $state(policy.userMFA);
let authMembershipsUserId = $state(policy.userId);
let authMembershipsUserName = $state(policy.userName);
let authMembershipsUserEmail = $state(policy.userEmail);
let authMembershipsUserPhone = $state(policy.userPhone);
const isSubmitDisabled = $derived(
authMembershipsUserId === (project?.authMembershipsUserId ?? true) &&
authMembershipsUserName === (project?.authMembershipsUserName ?? true) &&
authMembershipsUserEmail === (project?.authMembershipsUserEmail ?? true) &&
authMembershipsUserPhone === (project?.authMembershipsUserPhone ?? true) &&
authMembershipsMfa === (project?.authMembershipsMfa ?? true)
authMembershipsUserId === policy.userId &&
authMembershipsUserName === policy.userName &&
authMembershipsUserEmail === policy.userEmail &&
authMembershipsUserPhone === policy.userPhone &&
authMembershipsMfa === policy.userMFA
);
async function updateMembershipsPrivacy() {
@@ -1,6 +1,6 @@
<script lang="ts">
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { CardGrid, PaginationInline } from '$lib/components';
import Confirm from '$lib/components/confirm.svelte';
import { InputPhone, InputOTP, Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
@@ -13,7 +13,7 @@
import EmptyCardImageCloud from '$lib/components/emptyCardImageCloud.svelte';
import { app } from '$lib/stores/app';
import Empty from '$lib/components/empty.svelte';
import type { Models } from '@appwrite.io/console';
import { Query, type Models } from '@appwrite.io/console';
import { Icon, Input, Layout, Link, Spinner, Tooltip } from '@appwrite.io/pink-svelte';
import { IconPlus, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte';
@@ -32,6 +32,9 @@
let savedNumbers: MockNumberForm[] = $state([]);
let draftNumbers: MockNumberForm[] = $state([]);
let isLoadingMockNumbers = $state(false);
let mockNumbersTotal = $state(0);
let mockNumbersOffset = $state(0);
const mockNumbersLimit = 10;
let lastProjectId: string | null = $state(null);
let pendingRow: string | null = $state(null);
let showDeleteConfirm = $state(false);
@@ -79,7 +82,16 @@
}
const response = await sdk
.forProject(project.region, project.$id)
.project.listMockPhones({});
.project.listMockPhones({
queries: [Query.limit(mockNumbersLimit), Query.offset(mockNumbersOffset)]
});
if (response.total > 0 && response.mockNumbers.length === 0 && mockNumbersOffset > 0) {
mockNumbersOffset =
Math.floor((response.total - 1) / mockNumbersLimit) * mockNumbersLimit;
await loadMockNumbers();
return;
}
mockNumbersTotal = response.total;
savedNumbers = getMockNumberRows(response.mockNumbers ?? []);
} catch (error) {
addNotification({ type: 'error', message: error.message });
@@ -381,7 +393,7 @@
id={`draft-key-${index}`}
bind:value={number.number}
placeholder="Enter phone number"
label={savedNumbers.length === 0 && index === 0
label={mockNumbersTotal === 0 && index === 0
? 'Phone number'
: undefined}
minlength={9}
@@ -404,7 +416,7 @@
id={`draft-value-${index}`}
bind:value={number.otp}
placeholder="Enter value"
label={savedNumbers.length === 0 && index === 0
label={mockNumbersTotal === 0 && index === 0
? 'Verification code'
: undefined}
maxlength={6}
@@ -442,17 +454,24 @@
</Layout.Stack>
</Layout.Stack>
{/each}
{#if savedNumbers.length + draftNumbers.length < 10}
{#if mockNumbersTotal + draftNumbers.length < 10}
<div>
<Button
secondary
on:click={addPhoneNumber}
disabled={savedNumbers.length + draftNumbers.length >= 10}>
disabled={mockNumbersTotal + draftNumbers.length >= 10}>
<Icon icon={IconPlus} slot="start" size="s" />
Add number
</Button>
</div>
{/if}
{#if mockNumbersTotal > mockNumbersLimit}
<PaginationInline
bind:offset={mockNumbersOffset}
limit={mockNumbersLimit}
total={mockNumbersTotal}
on:change={loadMockNumbers} />
{/if}
{:else}
<Empty on:click={addPhoneNumber}>Add a number</Empty>
{/if}
@@ -11,12 +11,14 @@
import type { Models } from '@appwrite.io/console';
let {
project
project,
policy
}: {
project: Models.Project;
policy: Models.PolicySessionDuration;
} = $props();
const { value, unit, baseValue, units } = $derived(createTimeUnitPair(project?.authDuration));
const { value, unit, baseValue, units } = $derived(createTimeUnitPair(policy.duration));
const options = $derived(units.map((v) => ({ label: v.name, value: v.name })));
async function updateSessionLength() {
@@ -51,7 +53,7 @@
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={$baseValue === project.authDuration} on:click={updateSessionLength}>
<Button disabled={$baseValue === policy.duration} on:click={updateSessionLength}>
Update
</Button>
</svelte:fragment>
@@ -10,12 +10,14 @@
import type { Models } from '@appwrite.io/console';
let {
project
project,
policy
}: {
project: Models.Project;
policy: Models.PolicySessionLimit;
} = $props();
let maxSessions = $state(project?.authSessionsLimit);
let maxSessions = $state(policy.total);
async function updateSessionsLimit() {
try {
@@ -54,7 +56,7 @@
bind:value={maxSessions} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={maxSessions === project?.authSessionsLimit} submit>Update</Button>
<Button disabled={maxSessions === policy.total} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,157 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSwitch } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
type EnabledPolicy = {
$id: string;
enabled: boolean;
};
let {
project,
denyAliasedEmailPolicy,
denyDisposableEmailPolicy,
denyFreeEmailPolicy
}: {
project: Models.Project;
denyAliasedEmailPolicy: EnabledPolicy;
denyDisposableEmailPolicy: EnabledPolicy;
denyFreeEmailPolicy: EnabledPolicy;
} = $props();
const getInitialAliasedEmails = () => denyAliasedEmailPolicy.enabled;
const getInitialDisposableEmails = () => denyDisposableEmailPolicy.enabled;
const getInitialFreeEmails = () => denyFreeEmailPolicy.enabled;
let savedAliasedEmails = $state(getInitialAliasedEmails());
let savedDisposableEmails = $state(getInitialDisposableEmails());
let savedFreeEmails = $state(getInitialFreeEmails());
let authAliasedEmails = $state(getInitialAliasedEmails());
let authDisposableEmails = $state(getInitialDisposableEmails());
let authFreeEmails = $state(getInitialFreeEmails());
const hasChanges = $derived.by(() => {
const aliasedChanged = authAliasedEmails !== savedAliasedEmails;
const disposableChanged = authDisposableEmails !== savedDisposableEmails;
const freeChanged = authFreeEmails !== savedFreeEmails;
return aliasedChanged || disposableChanged || freeChanged;
});
async function updateSignupEmailSecurity() {
let currentSubmit = Submit.AuthAliasedEmailsUpdate;
let hasAppliedServerChange = false;
try {
const projectSdk = sdk.forProject(project.region, project.$id).project;
if (authAliasedEmails !== savedAliasedEmails) {
currentSubmit = Submit.AuthAliasedEmailsUpdate;
await projectSdk.updateDenyAliasedEmailPolicy({
enabled: authAliasedEmails
});
hasAppliedServerChange = true;
trackEvent(Submit.AuthAliasedEmailsUpdate);
}
if (authDisposableEmails !== savedDisposableEmails) {
currentSubmit = Submit.AuthDisposableEmailsUpdate;
await projectSdk.updateDenyDisposableEmailPolicy({
enabled: authDisposableEmails
});
hasAppliedServerChange = true;
trackEvent(Submit.AuthDisposableEmailsUpdate);
}
if (authFreeEmails !== savedFreeEmails) {
currentSubmit = Submit.AuthFreeEmailsUpdate;
await projectSdk.updateDenyFreeEmailPolicy({
enabled: authFreeEmails
});
hasAppliedServerChange = true;
trackEvent(Submit.AuthFreeEmailsUpdate);
}
savedAliasedEmails = authAliasedEmails;
savedDisposableEmails = authDisposableEmails;
savedFreeEmails = authFreeEmails;
await invalidate(Dependencies.PROJECT);
addNotification({
type: 'success',
message: 'Updated signup email security settings.'
});
} catch (error) {
if (hasAppliedServerChange) {
await invalidate(Dependencies.PROJECT);
}
addNotification({
type: 'error',
message: error.message
});
trackError(error, currentSubmit);
}
}
</script>
<Form onSubmit={updateSignupEmailSecurity}>
<CardGrid gap="xxl">
<svelte:fragment slot="title">Email policies</svelte:fragment>
Control which email addresses can be used for user creation and email updates. This does not affect
session creation (sign in)
<svelte:fragment slot="aside">
<InputSwitch bind:value={authFreeEmails} id="authFreeEmails" label="Deny free emails">
<svelte:fragment slot="description">
<Layout.Stack gap="s">
<Typography.Text
>Block email addresses of free email providers. For example: <Typography.Code
>user@gmail.com</Typography.Code>
</Typography.Text>
</Layout.Stack>
</svelte:fragment>
</InputSwitch>
<InputSwitch
bind:value={authAliasedEmails}
id="authAliasedEmails"
label="Deny aliased emails">
<svelte:fragment slot="description">
<Layout.Stack gap="s">
<Typography.Text>
Block emails with aliases, tags, subaddresses, or any other
provider-specific email variations. For example: <Typography.Code
>user+folder1@gmail.com</Typography.Code>
</Typography.Text>
</Layout.Stack>
</svelte:fragment>
</InputSwitch>
<InputSwitch
bind:value={authDisposableEmails}
id="authDisposableEmails"
label="Deny disposable emails">
<svelte:fragment slot="description">
<Layout.Stack gap="s">
<Typography.Text
>Block temporary and disposable email providers. For example: <Typography.Code
>alex9734@mailinator.com</Typography.Code>
</Typography.Text>
</Layout.Stack>
</svelte:fragment>
</InputSwitch>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -11,22 +11,21 @@
import type { Models } from '@appwrite.io/console';
let {
project
project,
policy
}: {
project: Models.Project;
policy: Models.PolicyUserLimit;
} = $props();
let maxUsersInputField: HTMLInputElement | null = $state(null);
let value = $state(project?.authLimit !== 0 ? 'limited' : 'unlimited');
let newLimit = $state(project?.authLimit !== 0 ? project?.authLimit : 100);
let value = $state(policy.total !== 0 ? 'limited' : 'unlimited');
let newLimit = $state(policy.total !== 0 ? policy.total : 100);
const isLimited = $derived(value === 'limited');
const btnDisabled = $derived.by(() => {
return (
(!isLimited && project?.authLimit === 0) ||
(isLimited && project?.authLimit === newLimit)
);
return (!isLimited && policy.total === 0) || (isLimited && policy.total === newLimit);
});
async function updateLimit() {
@@ -10,7 +10,7 @@
import { addNotification } from '$lib/stores/notifications';
import { oAuthProviders } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import { AuthMethod as MethodId, type Models } from '@appwrite.io/console';
import { ProjectAuthMethodId } from '@appwrite.io/console';
import { base } from '$app/paths';
import {
Avatar,
@@ -24,6 +24,7 @@
} from '@appwrite.io/pink-svelte';
import { Dependencies } from '$lib/constants';
import type { PageProps } from './$types';
import type { AuthProvider } from '../updateOAuth';
import { SvelteSet } from 'svelte/reactivity';
import { get } from 'svelte/store';
@@ -34,11 +35,11 @@
const consoleParamsMap = $derived(data.consoleParamsMap);
let showProvider = $state(false);
let selectedProvider: Models.AuthProvider | null = $state(null);
let selectedProvider: AuthProvider | null = $state(null);
let isUpdatingAllAuthMethods = $state(false);
let showUpdateAuthMethodsDialog = $state(false);
let updateAuthMethodsEnabledMode = $state<boolean | null>(null);
let apiAuthMethodUpdates = new SvelteSet<MethodId>();
let apiAuthMethodUpdates = new SvelteSet<ProjectAuthMethodId>();
const isAnyAuthMethodUpdating = $derived(apiAuthMethodUpdates.size > 0);
const isAnyUpdateInProgress = $derived(isUpdatingAllAuthMethods || isAnyAuthMethodUpdating);
@@ -1,25 +1,9 @@
import { Dependencies } from '$lib/constants';
import { oAuthProviders } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import type { Models, Models as ConsoleModels } from '@appwrite.io/console';
import type { Models as ConsoleModels } from '@appwrite.io/console';
import type { PageLoad } from './$types';
/** Extract the appId equivalent from a typed OAuth2 provider model. */
function extractAppId(p: Record<string, unknown>): string {
return (p['clientId'] ??
p['appId'] ??
p['oauthClientId'] ??
p['oauth2ClientId'] ??
p['applicationId'] ??
p['serviceId'] ??
p['apiKey'] ??
p['publicKey'] ??
p['appKey'] ??
p['keyString'] ??
p['customerKey'] ??
p['key'] ??
'') as string;
}
import type { AuthProvider } from '../updateOAuth';
export const load: PageLoad = async ({ depends, params }) => {
depends(Dependencies.PROJECT);
@@ -34,19 +18,13 @@ export const load: PageLoad = async ({ depends, params }) => {
consoleParamsMap.set(cp.$id, cp.parameters ?? []);
}
const oauthProviders: Models.AuthProvider[] = providerList.providers
const oauthProviders: AuthProvider[] = providerList.providers
.filter((p) => p.$id !== 'mock')
.map((p) => {
const raw = p as unknown as Record<string, unknown>;
return {
...raw,
key: p.$id,
name: oAuthProviders[p.$id]?.name ?? p.$id,
appId: extractAppId(raw),
secret: '',
enabled: p.enabled
};
});
.map((p) => ({
...p,
key: p.$id,
name: oAuthProviders[p.$id]?.name ?? p.$id
}));
return { oauthProviders, consoleParamsMap };
};
@@ -1,5 +1,9 @@
<script module lang="ts">
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
@@ -8,8 +12,8 @@
export async function loadEmailTemplate(
region: string,
projectId: string,
type: EmailTemplateType,
locale: EmailTemplateLocale = EmailTemplateLocale.En
type: ProjectEmailTemplateId,
locale: ProjectEmailTemplateLocale = ProjectEmailTemplateLocale.En
): Promise<EmailTemplateForm> {
try {
const template = await sdk.forProject(region, projectId).project.getEmailTemplate({
@@ -27,8 +31,8 @@
function normalizeEmailTemplate(
template: Models.EmailTemplate,
type: EmailTemplateType,
locale: EmailTemplateLocale = EmailTemplateLocale.En
type: ProjectEmailTemplateId,
locale: ProjectEmailTemplateLocale = ProjectEmailTemplateLocale.En
): EmailTemplateForm {
return {
...template,
@@ -58,9 +62,9 @@
let isTemplateLoading = $state(false);
let openStates = $state(Object.fromEntries(templates.map(({ key }) => [key, false])));
loadTemplateFor(EmailTemplateType.Verification);
loadTemplateFor(ProjectEmailTemplateId.Verification);
async function loadTemplateFor(type: EmailTemplateType) {
async function loadTemplateFor(type: ProjectEmailTemplateId) {
// return, already loaded!
if (templateType === type) return;
@@ -71,14 +75,14 @@
page.params.region,
page.params.project,
type,
EmailTemplateLocale.En
ProjectEmailTemplateLocale.En
);
$baseEmailTemplate = { ...$emailTemplate };
isTemplateLoading = false;
}
function toggleAccordion(type: EmailTemplateType) {
function toggleAccordion(type: ProjectEmailTemplateId) {
for (const key in openStates) {
openStates[key] = false;
}
@@ -7,14 +7,18 @@
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout, Card } from '@appwrite.io/pink-svelte';
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
export let loading = false;
export let project: Models.Project;
export let localeCodes: Models.LocaleCode[];
let isUpdating = false;
let locale = EmailTemplateLocale.En;
let locale = ProjectEmailTemplateLocale.En;
async function onLocaleChange() {
const timeout = setTimeout(() => {
@@ -24,12 +28,15 @@
const template = await loadEmailTemplate(
project.region,
project.$id,
EmailTemplateType.MfaChallenge,
ProjectEmailTemplateId.MfaChallenge,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.MfaChallenge });
trackEvent(Submit.EmailChangeLocale, {
locale,
type: ProjectEmailTemplateId.MfaChallenge
});
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -7,14 +7,18 @@
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout, Card } from '@appwrite.io/pink-svelte';
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
export let loading = false;
export let project: Models.Project;
export let localeCodes: Models.LocaleCode[];
let isUpdating = false;
let locale = EmailTemplateLocale.En;
let locale = ProjectEmailTemplateLocale.En;
async function onLocaleChange() {
const timeout = setTimeout(() => {
@@ -24,12 +28,15 @@
const template = await loadEmailTemplate(
project.region,
project.$id,
EmailTemplateType.Invitation,
ProjectEmailTemplateId.Invitation,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Invitation });
trackEvent(Submit.EmailChangeLocale, {
locale,
type: ProjectEmailTemplateId.Invitation
});
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -7,14 +7,18 @@
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout, Card } from '@appwrite.io/pink-svelte';
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
export let loading = false;
export let project: Models.Project;
export let localeCodes: Models.LocaleCode[];
let isUpdating = false;
let locale = EmailTemplateLocale.En;
let locale = ProjectEmailTemplateLocale.En;
async function onLocaleChange() {
const timeout = setTimeout(() => {
@@ -24,12 +28,15 @@
const template = await loadEmailTemplate(
project.region,
project.$id,
EmailTemplateType.MagicSession,
ProjectEmailTemplateId.MagicSession,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.MagicSession });
trackEvent(Submit.EmailChangeLocale, {
locale,
type: ProjectEmailTemplateId.MagicSession
});
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -7,14 +7,18 @@
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout, Card } from '@appwrite.io/pink-svelte';
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
export let loading = false;
export let project: Models.Project;
export let localeCodes: Models.LocaleCode[];
let isUpdating = false;
let locale = EmailTemplateLocale.En;
let locale = ProjectEmailTemplateLocale.En;
async function onLocaleChange() {
const timeout = setTimeout(() => {
@@ -24,12 +28,15 @@
const template = await loadEmailTemplate(
project.region,
project.$id,
EmailTemplateType.OtpSession,
ProjectEmailTemplateId.OtpSession,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.OtpSession });
trackEvent(Submit.EmailChangeLocale, {
locale,
type: ProjectEmailTemplateId.OtpSession
});
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -7,14 +7,18 @@
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout, Card } from '@appwrite.io/pink-svelte';
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
export let loading = false;
export let project: Models.Project;
export let localeCodes: Models.LocaleCode[];
let isUpdating = false;
let locale = EmailTemplateLocale.En;
let locale = ProjectEmailTemplateLocale.En;
async function onLocaleChange() {
const timeout = setTimeout(() => {
@@ -24,12 +28,12 @@
const template = await loadEmailTemplate(
project.region,
project.$id,
EmailTemplateType.Recovery,
ProjectEmailTemplateId.Recovery,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Recovery });
trackEvent(Submit.EmailChangeLocale, { locale, type: ProjectEmailTemplateId.Recovery });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -7,14 +7,18 @@
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout, Card } from '@appwrite.io/pink-svelte';
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
export let loading = false;
export let project: Models.Project;
export let localeCodes: Models.LocaleCode[];
let isUpdating = false;
let locale = EmailTemplateLocale.En;
let locale = ProjectEmailTemplateLocale.En;
async function onLocaleChange() {
const timeout = setTimeout(() => {
@@ -24,12 +28,15 @@
const template = await loadEmailTemplate(
project.region,
project.$id,
EmailTemplateType.SessionAlert,
ProjectEmailTemplateId.SessionAlert,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.SessionAlert });
trackEvent(Submit.EmailChangeLocale, {
locale,
type: ProjectEmailTemplateId.SessionAlert
});
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -5,7 +5,11 @@
import { baseEmailTemplate, emailTemplate } from './store';
import deepEqual from 'deep-equal';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { EmailTemplateLocale, type EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
type ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
import { Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import TemplateSkeleton from './templateSkeleton.svelte';
@@ -29,7 +33,8 @@
const isButtonDisabled = $derived(deepEqual($emailTemplate, $baseEmailTemplate));
async function saveEmailTemplate() {
const locale = ($emailTemplate.locale || EmailTemplateLocale.En) as EmailTemplateLocale;
const locale = ($emailTemplate.locale ||
ProjectEmailTemplateLocale.En) as ProjectEmailTemplateLocale;
// TODO: uncomment after SDK is updated
// if (!isValueOfStringEnum(TemplateType, $emailTemplate.type)) {
@@ -54,7 +59,7 @@
break;
}
await sdk.forProject(project.region, project.$id).project.updateEmailTemplate({
templateId: $emailTemplate.type as EmailTemplateType,
templateId: $emailTemplate.type as ProjectEmailTemplateId,
locale,
subject: $emailTemplate.subject || undefined,
message: $emailTemplate.message || undefined,
@@ -7,14 +7,18 @@
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Layout, Card } from '@appwrite.io/pink-svelte';
import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateLocale,
ProjectEmailTemplateId,
type Models
} from '@appwrite.io/console';
export let loading = false;
export let project: Models.Project;
export let localeCodes: Models.LocaleCode[];
let isUpdating = false;
let locale = EmailTemplateLocale.En;
let locale = ProjectEmailTemplateLocale.En;
async function onLocaleChange() {
const timeout = setTimeout(() => {
@@ -24,12 +28,15 @@
const template = await loadEmailTemplate(
project.region,
project.$id,
EmailTemplateType.Verification,
ProjectEmailTemplateId.Verification,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Verification });
trackEvent(Submit.EmailChangeLocale, {
locale,
type: ProjectEmailTemplateId.Verification
});
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -1,5 +1,9 @@
import { writable } from 'svelte/store';
import { EmailTemplateType, type EmailTemplateLocale, type Models } from '@appwrite.io/console';
import {
ProjectEmailTemplateId,
type ProjectEmailTemplateLocale,
type Models
} from '@appwrite.io/console';
// component imports
import Email2FaTemplate from './email2FATemplate.svelte';
@@ -11,9 +15,9 @@ import EmailVerificationTemplate from './emailVerificationTemplate.svelte';
import EmailSessionAlertTemplate from './emailSessionAlertTemplate.svelte';
export type EmailTemplateForm = Models.EmailTemplate & {
type: EmailTemplateType | null;
templateId: EmailTemplateType | string | null;
locale: EmailTemplateLocale | string | null;
type: ProjectEmailTemplateId | null;
templateId: ProjectEmailTemplateId | string | null;
locale: ProjectEmailTemplateLocale | string | null;
};
export const emailTemplate = writable<EmailTemplateForm>({
@@ -42,44 +46,44 @@ export const baseEmailTemplate = writable<EmailTemplateForm>({
export const templates = [
{
key: EmailTemplateType.Verification,
key: ProjectEmailTemplateId.Verification,
title: 'Verification',
description:
'Send a verification email to users that sign in with their email and password.',
component: EmailVerificationTemplate
},
{
key: EmailTemplateType.MagicSession,
key: ProjectEmailTemplateId.MagicSession,
title: 'Magic URL',
description: 'Send an email to users that sign in with a magic URL.',
component: EmailMagicUrlTemplate
},
{
key: EmailTemplateType.OtpSession,
key: ProjectEmailTemplateId.OtpSession,
title: 'OTP session',
description: 'Send an email to users that sign in with a email OTP.',
component: EmailOtpSessionTemplate
},
{
key: EmailTemplateType.Recovery,
key: ProjectEmailTemplateId.Recovery,
title: 'Reset password',
description: 'Send a recovery email to users that forget their password.',
component: EmailRecoveryTemplate
},
{
key: EmailTemplateType.Invitation,
key: ProjectEmailTemplateId.Invitation,
title: 'Invite user',
description: 'Send an invitation email to become a member of your project.',
component: EmailInviteTemplate
},
{
key: EmailTemplateType.MfaChallenge,
key: ProjectEmailTemplateId.MfaChallenge,
title: '2FA verification',
description: 'Send a two-factor authentication email to a user.',
component: Email2FaTemplate
},
{
key: EmailTemplateType.SessionAlert,
key: ProjectEmailTemplateId.SessionAlert,
title: 'Session alert',
description: 'Send an email to users when a new session is created.',
component: EmailSessionAlertTemplate,
@@ -4,14 +4,21 @@ import { Dependencies } from '$lib/constants';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { OAuthProvider, type Models } from '@appwrite.io/console';
import { OAuthProvider, type Models as ConsoleModels } from '@appwrite.io/console';
type ProjectOAuthProvider = ConsoleModels.OAuth2ProviderList['providers'][number];
export type AuthProvider = ProjectOAuthProvider & {
key: ProjectOAuthProvider['$id'];
name: string;
};
type Args = {
region: string;
projectId: string;
provider: Models.AuthProvider;
provider: AuthProvider;
appId: string | null;
secret: string;
secret: string | null;
details: Record<string, string>;
enabled: boolean;
};
@@ -21,7 +28,7 @@ type Return = {
message?: string;
};
function parseSecret(secret: string) {
function parseSecret(secret: string | null) {
if (!secret) return {};
try {
@@ -10,16 +10,20 @@
import { onMount } from 'svelte';
import { func } from '../store';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { Runtime, type Scopes as ScopesType } from '@appwrite.io/console';
import {
Runtime,
type ProjectKeyScopes,
type Scopes as FunctionScopes
} from '@appwrite.io/console';
import Scopes from '$routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte';
import { symmetricDifference } from '$lib/helpers/array';
import { Link } from '$lib/elements';
const functionId = page.params.function;
let functionScopes: ScopesType[] = null;
let functionScopes: ProjectKeyScopes[] = null;
onMount(async () => {
functionScopes ??= $func.scopes as ScopesType[];
functionScopes ??= $func.scopes as ProjectKeyScopes[];
});
async function updateScopes() {
@@ -39,7 +43,7 @@
logging: $func.logging || undefined,
entrypoint: $func.entrypoint || undefined,
commands: $func.commands || undefined,
scopes: functionScopes || undefined,
scopes: (functionScopes as unknown as FunctionScopes[]) || undefined,
installationId: $func.installationId || undefined,
providerRepositoryId: $func.providerRepositoryId || undefined,
providerBranch: $func.providerBranch || undefined,
@@ -7,4 +7,4 @@
<title>Console - Appwrite</title>
</svelte:head>
<Onboard platforms={page.data.project.platforms} pingCount={page.data.project.pingCount} />
<Onboard platforms={page.data.platforms.platforms} pingCount={page.data.project.pingCount} />
@@ -13,8 +13,8 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
import Scopes from '../api-keys/scopes.svelte';
import { ID, type Scopes as ScopesType } from '@appwrite.io/console';
import ScopesSelector from '../api-keys/scopes.svelte';
import { ID, type ProjectKeyScopes } from '@appwrite.io/console';
import { page } from '$app/state';
import { copy } from '$lib/helpers/copy';
@@ -22,7 +22,7 @@
let formComponent: Form;
let isSubmitting = writable(false);
let scopes: ScopesType[] = [];
let scopes: ProjectKeyScopes[] = [];
let name = '';
let expire: string | null = null;
@@ -101,7 +101,7 @@
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project goals.
</Typography.Text>
<Scopes bind:scopes />
<ScopesSelector bind:scopes />
</Layout.Stack>
</Fieldset>
</Layout.Stack>
@@ -12,16 +12,16 @@
import { project } from '../../store';
import Delete from './delete.svelte';
import UpdateExpirationDate from './updateExpirationDate.svelte';
import type { Models, Scopes as ScopesType } from '@appwrite.io/console';
import type { Models, ProjectKeyScopes } from '@appwrite.io/console';
import { symmetricDifference } from '$lib/helpers/array';
import Scopes from '../api-keys/scopes.svelte';
import ScopesSelector from '../api-keys/scopes.svelte';
import { InteractiveText, Layout, Typography } from '@appwrite.io/pink-svelte';
export let key: Models.DevKey | Models.Key;
export let keyType: 'api' | 'dev' = 'api';
let name: string = null;
let scopes: ScopesType[] = null;
let scopes: ProjectKeyScopes[] = null;
let showDelete = false;
const isApiKey = keyType === 'api';
@@ -32,7 +32,7 @@
onMount(() => {
name ??= key.name;
if (isApiKey) {
scopes ??= (key as Models.Key).scopes as ScopesType[];
scopes ??= (key as Models.Key).scopes as ProjectKeyScopes[];
}
});
@@ -166,7 +166,7 @@
to allow only the permissions you need to meet your project goals.
<svelte:fragment slot="aside">
{#if scopes !== null}
<Scopes bind:scopes />
<ScopesSelector bind:scopes />
{/if}
</svelte:fragment>
@@ -2,7 +2,7 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { Empty, MultiSelectionTable } from '$lib/components';
import { Empty, MultiSelectionTable, PaginationWithLimit } from '$lib/components';
import { canWriteKeys } from '$lib/stores/roles';
import type { Models } from '@appwrite.io/console';
import { diffDays } from '$lib/helpers/date';
@@ -14,10 +14,14 @@
let {
keyType = 'api',
keys
keys,
limit,
offset
}: {
keyType?: 'api' | 'dev';
keys: Models.KeyList | Models.DevKeyList;
limit?: number;
offset?: number;
} = $props();
let selectedKeys = $state([]);
@@ -116,6 +120,13 @@
{/each}
{/snippet}
</MultiSelectionTable>
{#if limit !== undefined && offset !== undefined}
<PaginationWithLimit
name={`${capitalize(label)} keys`}
{limit}
{offset}
total={keys.total} />
{/if}
{:else}
<Empty
single
@@ -9,7 +9,7 @@
import { sdk } from '$lib/stores/sdk';
import { Alert } from '@appwrite.io/pink-svelte';
import { ExpirationInput } from '$lib/components';
import type { Models, Scopes } from '@appwrite.io/console';
import type { Models, ProjectKeyScopes } from '@appwrite.io/console';
import { page } from '$app/state';
export let keyType: 'api' | 'dev' = 'api';
@@ -31,7 +31,7 @@
await sdk.forProject(page.params.region, page.params.project).project.updateKey({
keyId: key.$id,
name: key.name,
scopes: (key as Models.Key).scopes as Scopes[],
scopes: (key as Models.Key).scopes as ProjectKeyScopes[],
expire: expiration
});
} else {
@@ -9,4 +9,4 @@
setOverviewAction(Action);
</script>
<Table keys={data.keys} />
<Table keys={data.keys} limit={data.limit} offset={data.offset} />
@@ -1,10 +1,21 @@
import { Dependencies } from '$lib/constants';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, depends }) => {
export const load: PageLoad = async ({ params, depends, route, url }) => {
depends(Dependencies.KEYS);
const projectSdk = sdk.forProject(params.region, params.project).project;
const page = getPage(url);
const limit = getLimit(url, route, CARD_LIMIT);
const offset = pageToOffset(page, limit);
return {
keys: await sdk.forProject(params.region, params.project).project.listKeys()
limit,
offset,
keys: await projectSdk.listKeys({
queries: [Query.limit(limit), Query.offset(offset)]
})
};
};
@@ -46,9 +46,9 @@
Selector,
Typography
} from '@appwrite.io/pink-svelte';
import type { Scopes } from '@appwrite.io/console';
import type { ProjectKeyScopes } from '@appwrite.io/console';
let { scopes = $bindable([]) }: { scopes: Scopes[] } = $props();
let { scopes = $bindable([]) }: { scopes: ProjectKeyScopes[] } = $props();
let allScopesList: ScopeDefinition[] = $state([]);
let mounted = $state(false);
@@ -192,10 +192,10 @@
});
}
function generateSyncedScopes(activeScopesObj: Record<string, boolean>): Scopes[] {
function generateSyncedScopes(activeScopesObj: Record<string, boolean>): ProjectKeyScopes[] {
return Object.entries(activeScopesObj)
.filter(([scope, isActive]) => isActive && scopeCatalog.has(scope))
.map(([scope]) => scope as Scopes);
.map(([scope]) => scope as ProjectKeyScopes);
}
</script>
@@ -93,7 +93,8 @@
import {
type DeleteOperationState,
type DeleteOperation,
MultiSelectionTable
MultiSelectionTable,
PaginationWithLimit
} from '$lib/components';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError } from '$lib/actions/analytics';
@@ -200,6 +201,11 @@
{/each}
{/snippet}
</MultiSelectionTable>
<PaginationWithLimit
name="Platforms"
limit={data.limit}
offset={data.offset}
total={data.platforms.total} />
{:else}
<Card.Base padding="none">
<Empty
@@ -1,11 +1,20 @@
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { project } = await parent();
export const load: PageLoad = async ({ depends, params, route, url }) => {
depends(Dependencies.PROJECT);
const page = getPage(url);
const limit = getLimit(url, route, CARD_LIMIT);
const offset = pageToOffset(page, limit);
return {
platforms: {
platforms: project.platforms,
total: project.platforms.length
}
limit,
offset,
platforms: await sdk.forProject(params.region, params.project).project.listPlatforms({
queries: [Query.limit(limit), Query.offset(offset)]
})
};
};
@@ -14,7 +14,7 @@
import Details from './details.svelte';
import ExportModal from './exportModal.svelte';
import { readOnly } from '$lib/stores/billing';
import { ID, Scopes, type Models } from '@appwrite.io/console';
import { ID, ProjectKeyScopes, type Models } from '@appwrite.io/console';
import { canWriteProjects } from '$lib/stores/roles';
import {
IconCloud,
@@ -92,23 +92,23 @@
keyId: ID.unique(),
name: `[AUTO-GENERATED] Migration ${new Date().toISOString()}`,
scopes: [
Scopes.UsersRead,
Scopes.TeamsRead,
Scopes.DatabasesRead,
Scopes.CollectionsRead /* legacy */,
Scopes.AttributesRead /* legacy */,
Scopes.IndexesRead,
Scopes.DocumentsRead /* legacy */,
Scopes.TablesRead,
Scopes.ColumnsRead,
Scopes.RowsRead,
Scopes.FilesRead,
Scopes.BucketsRead,
Scopes.FunctionsRead,
Scopes.ExecutionsRead,
Scopes.LocaleRead,
Scopes.AvatarsRead,
Scopes.HealthRead
ProjectKeyScopes.UsersRead,
ProjectKeyScopes.TeamsRead,
ProjectKeyScopes.DatabasesRead,
ProjectKeyScopes.CollectionsRead /* legacy */,
ProjectKeyScopes.AttributesRead /* legacy */,
ProjectKeyScopes.IndexesRead,
ProjectKeyScopes.DocumentsRead /* legacy */,
ProjectKeyScopes.TablesRead,
ProjectKeyScopes.ColumnsRead,
ProjectKeyScopes.RowsRead,
ProjectKeyScopes.FilesRead,
ProjectKeyScopes.BucketsRead,
ProjectKeyScopes.FunctionsRead,
ProjectKeyScopes.ExecutionsRead,
ProjectKeyScopes.LocaleRead,
ProjectKeyScopes.AvatarsRead,
ProjectKeyScopes.HealthRead
]
});
@@ -10,7 +10,7 @@
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { organization } from '$lib/stores/organization';
import { ID, Scopes } from '@appwrite.io/console';
import { ID, ProjectKeyScopes } from '@appwrite.io/console';
export let show = false;
@@ -94,28 +94,28 @@
keyId: ID.unique(),
name: `[AUTO-GENERATED] Migration ${new Date().toISOString()}`,
scopes: [
Scopes.UsersRead,
Scopes.TeamsRead,
Scopes.DatabasesRead,
Scopes.CollectionsRead /* legacy */,
Scopes.AttributesRead /* legacy */,
Scopes.IndexesRead,
Scopes.DocumentsRead /* legacy */,
Scopes.TablesRead,
Scopes.ColumnsRead,
Scopes.RowsRead,
Scopes.FilesRead,
Scopes.BucketsRead,
Scopes.FunctionsRead,
Scopes.ExecutionsRead,
Scopes.SitesRead,
Scopes.ProvidersRead,
Scopes.TopicsRead,
Scopes.SubscribersRead,
Scopes.MessagesRead,
Scopes.LocaleRead,
Scopes.AvatarsRead,
Scopes.HealthRead
ProjectKeyScopes.UsersRead,
ProjectKeyScopes.TeamsRead,
ProjectKeyScopes.DatabasesRead,
ProjectKeyScopes.CollectionsRead /* legacy */,
ProjectKeyScopes.AttributesRead /* legacy */,
ProjectKeyScopes.IndexesRead,
ProjectKeyScopes.DocumentsRead /* legacy */,
ProjectKeyScopes.TablesRead,
ProjectKeyScopes.ColumnsRead,
ProjectKeyScopes.RowsRead,
ProjectKeyScopes.FilesRead,
ProjectKeyScopes.BucketsRead,
ProjectKeyScopes.FunctionsRead,
ProjectKeyScopes.ExecutionsRead,
ProjectKeyScopes.SitesRead,
ProjectKeyScopes.ProvidersRead,
ProjectKeyScopes.TopicsRead,
ProjectKeyScopes.SubscribersRead,
ProjectKeyScopes.MessagesRead,
ProjectKeyScopes.LocaleRead,
ProjectKeyScopes.AvatarsRead,
ProjectKeyScopes.HealthRead
]
});
@@ -12,7 +12,7 @@
import { resolve } from '$app/paths';
import deepEqual from 'deep-equal';
import { currentPlan } from '$lib/stores/organization';
import { type Secure } from '@appwrite.io/console';
import { type ProjectSMTPSecure } from '@appwrite.io/console';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import { getChangePlanUrl } from '$lib/stores/billing';
import { Link, Selector, Alert } from '@appwrite.io/pink-svelte';
@@ -85,7 +85,7 @@
port: port || undefined,
username: username || undefined,
password: password || undefined,
secure: secure ? (secure as Secure) : undefined
secure: secure ? (secure as ProjectSMTPSecure) : undefined
});
invalidate(Dependencies.PROJECT);
@@ -212,7 +212,7 @@
placeholder="Enter password" />
<InputSelect
id="tls"
label="Secure protocol"
label="ProjectSMTPSecure protocol"
placeholder="Select protocol"
bind:value={secure}
{options} />
@@ -11,18 +11,18 @@
import Button from '$lib/elements/forms/button.svelte';
import { Dialog, Divider, Layout, Spinner } from '@appwrite.io/pink-svelte';
import { SvelteSet } from 'svelte/reactivity';
import { ProtocolId } from '@appwrite.io/console';
import { ProjectProtocolId } from '@appwrite.io/console';
import { get } from 'svelte/store';
import { canWriteProjects } from '$lib/stores/roles';
let isUpdatingAllProtocols = $state(false);
let showUpdateProtocolDialog = $state(false);
let updateProtocolsEnabledMode = $state<boolean | null>(null);
let apiProtocolUpdates = new SvelteSet<ProtocolId>();
const protocolDescriptions: Record<ProtocolId, string> = {
[ProtocolId.Rest]: 'Standard HTTP API requests from client SDKs.',
[ProtocolId.Graphql]: 'GraphQL API access for queries and mutations.',
[ProtocolId.Websocket]: 'Realtime subscriptions over WebSocket connections.'
let apiProtocolUpdates = new SvelteSet<ProjectProtocolId>();
const protocolDescriptions: Record<ProjectProtocolId, string> = {
[ProjectProtocolId.Rest]: 'Standard HTTP API requests from client SDKs.',
[ProjectProtocolId.Graphql]: 'GraphQL API access for queries and mutations.',
[ProjectProtocolId.Websocket]: 'Realtime subscriptions over WebSocket connections.'
};
const isAnyProtocolUpdating = $derived(apiProtocolUpdates.size > 0);
const isAnyUpdateInProgress = $derived(isUpdatingAllProtocols || isAnyProtocolUpdating);
@@ -10,7 +10,7 @@
import { project } from '../store';
import Button from '$lib/elements/forms/button.svelte';
import { Dialog, Divider, Layout, Spinner } from '@appwrite.io/pink-svelte';
import type { ServiceId } from '@appwrite.io/console';
import type { ProjectServiceId } from '@appwrite.io/console';
import { get } from 'svelte/store';
import { SvelteSet } from 'svelte/reactivity';
@@ -20,7 +20,7 @@
let showUpdateServiceDialog = $state(false);
let updateServicesEnabledMode = $state<boolean | null>(null);
let apiServiceUpdates = new SvelteSet<ServiceId>();
let apiServiceUpdates = new SvelteSet<ProjectServiceId>();
const isAnyServiceUpdating = $derived(apiServiceUpdates.size > 0);
const isAnyUpdateInProgress = $derived(isUpdatingAllServices || isAnyServiceUpdating);
@@ -22,8 +22,8 @@ export const projectRegion = derived(project, ($project) => {
});
export const onboarding = derived(
project,
($project) => $project?.platforms?.length === 0 && $project?.keys?.length === 0
page,
($page) => $page.data.platforms?.total === 0 && $page.data.keys?.total === 0
);
/**
+21 -3
View File
@@ -2,21 +2,39 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import { Layout, Spinner, Typography } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
onMount(async () => {
const params = new URLSearchParams(window.location.search);
const projectId = params.get('projectId');
const origin = params.get('origin');
const path = params.get('path');
try {
await sdk.forConsole.projects.get({ projectId });
const { projects } = await sdk.forConsole.projects.list({
queries: [
Query.equal('$id', projectId),
Query.limit(1),
Query.select(['$id', 'region'])
],
total: false
});
const project = projects[0];
if (!project) {
await goto(
`${base}/auth/preview/access?origin=${origin}&path=${path}&projectId=${projectId}`
);
return;
}
await sdk.forProject(project.region ?? 'default', projectId).project.get();
const jwt = await sdk.forConsole.account.createJWT();
window.location.href = `${origin}/_appwrite/authorize?jwt=${jwt.jwt}&path=${path}`;
} catch {
goto(
await goto(
`${base}/auth/preview/access?origin=${origin}&path=${path}&projectId=${projectId}`
);
}