Skip to content

feat: add signup email security controls + ce refactor changes#3039

Merged
HarshMN2345 merged 25 commits into
mainfrom
feat-signup-email-security
May 14, 2026
Merged

feat: add signup email security controls + ce refactor changes#3039
HarshMN2345 merged 25 commits into
mainfrom
feat-signup-email-security

Conversation

@HarshMN2345
Copy link
Copy Markdown
Member

@HarshMN2345 HarshMN2345 commented May 11, 2026

What does this PR do?

Uploading image.png…

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

(Write your answer here.)

@HarshMN2345 HarshMN2345 requested a review from Meldiron May 11, 2026 06:31
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Greptile Summary

This PR introduces three signup email security controls (deny aliased/disposable/free emails) and performs a broad refactoring of the auth security page to consume SDK policy objects instead of flat project model fields. It also adds pagination to mock phone numbers, rewires onboarding progress tracking to use per-load list calls, and aligns enum names with a new @appwrite.io/console SDK version pinned via commit hash.

  • New updateSignupEmailSecurity component: Adds three toggles backed by denyAliasedEmailPolicy, denyDisposableEmailPolicy, and denyFreeEmailPolicy; uses currentSubmit tracking and a hasAppliedServerChange flag for partial-failure invalidation.
  • Policy-object refactor: All security components now receive typed policy objects from +page.ts instead of reading flat project model fields.
  • Onboarding progress tracking: +layout.ts now fetches listPlatforms and listKeys (both limit:1) in the top-level Promise.all, and the onboarding store derives state from page.data.platforms.total and page.data.keys.total.

Confidence Score: 3/5

Multiple API calls were added to critical load paths without individual error boundaries, meaning transient failures can make entire pages inaccessible.

The project layout now includes listPlatforms and listKeys in its top-level Promise.all; a failure in either drops every project sub-page. The org overview uses Promise.all over per-project platform fetches, so one unavailable region breaks the entire org dashboard. The security page returns bare-cast policy objects with no null fallback for nine of twelve policies. The core SDK was moved from a verified npm release to an unverified commit-hash URL.

src/routes/(console)/project-[region]-[project]/+layout.ts, src/routes/(console)/organization-[organization]/+page.ts, src/routes/(console)/project-[region]-[project]/auth/security/+page.ts, package.json

Important Files Changed

Filename Overview
package.json Core SDK changed from semver-pinned npm release to an unversioned pkg.vc commit URL; bun.lock omits the sha512 integrity hash, removing supply-chain verification for the application's primary dependency.
src/routes/(console)/project-[region]-[project]/+layout.ts listPlatforms and listKeys added to the top-level Promise.all; a transient failure in either call blocks every project sub-page, not just onboarding progress.
src/routes/(console)/organization-[organization]/+page.ts Parallelised platform fetches with Promise.all; a single project's listPlatforms failure rejects the whole array and renders the entire organization overview inaccessible.
src/routes/(console)/project-[region]-[project]/auth/security/+page.ts New file loads all project policies via a single listPolicies call; non-email policies are returned with bare type-casts and no null fallback, meaning a partial backend response will crash every security child component.
src/routes/(console)/project-[region]-[project]/auth/security/updateSignupEmailSecurity.svelte New component initialises state directly from policy props, uses currentSubmit tracking, and invalidates on partial failure; the main new feature component looks correct.
src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte Adds server-side pagination with offset tracking and an empty-page guard; the Add-button cap correctly uses mockNumbersTotal rather than the current page's savedNumbers length.
src/lib/stores/project-services.ts Refactored from direct flat-field access to a Map lookup; if project.services is absent or a service entry is missing, the value silently falls through to null.
src/routes/(console)/organization-[organization]/+page.svelte Overflow badge corrected to use platformsTotal > 2 (fixing old off-by-one). Adds ?create-project URL-param effect with a handled-guard.

Reviews (22): Last reviewed commit: "Improve copy" | Re-trigger Greptile

Comment thread src/routes/(public)/auth/preview/+page.svelte Outdated
Comment thread src/routes/(console)/project-[region]-[project]/overview/api-keys/[key]/store.ts Outdated
Comment thread src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte Outdated
Comment on lines +131 to +132
platforms: platformList,
keys: keyList,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need platforms and keys for layout, or can layout render without it - and then we load those in releavnt +page.ts (for dashboard)?

Comment thread src/routes/(console)/project-[region]-[project]/+layout.ts Outdated
</svelte:head>

<Onboard platforms={page.data.project.platforms} pingCount={page.data.project.pingCount} />
<Onboard platforms={page.data.platforms.platforms} pingCount={page.data.project.pingCount} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

platforms platforms isnt correct I think 🤔 Why not just page.data.platforms?

I might be wrong tho. Is there other places where we have this double-name pattern? Maybe something iwth pagination?

Comment thread src/routes/(console)/project-[region]-[project]/auth/updateOAuth.ts Outdated
Comment thread src/lib/actions/analytics.ts Outdated
Comment thread src/lib/layout/shell.svelte
Comment thread src/lib/stores/page.ts Outdated
Comment thread src/routes/(console)/organization-[organization]/+page.svelte
Comment thread src/routes/(console)/organization-[organization]/+page.svelte Outdated
Comment thread src/routes/(console)/organization-[organization]/+page.svelte Outdated
Comment thread src/routes/(console)/organization-[organization]/+page.ts Outdated
Comment thread src/routes/(console)/project-[region]-[project]/auth/security/+page.ts Outdated
Comment thread src/routes/(console)/organization-[organization]/+page.ts Outdated
Comment thread src/routes/(console)/organization-[organization]/+page.ts Outdated
Comment on lines +11 to +16
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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Silent null fallback masks missing service entries

The same pattern applied across project-services.ts, auth-methods.ts, and project-protocols.ts: if project.services (or .authMethods / .protocols) is absent or doesn't contain an entry for a given ID, services.get(...) returns undefined and ?? null yields null for that toggle. Previously these read flat boolean fields (project?.serviceStatusForAccount), so null only occurred when the entire project was null. Now any service/method/protocol missing from the backend array silently renders as indeterminate — and any UI that treats null the same as false (disabled) would silently suppress what may actually be an enabled setting.

@HarshMN2345 HarshMN2345 force-pushed the feat-signup-email-security branch from 2708bc4 to 13382b6 Compare May 12, 2026 16:07
@HarshMN2345 HarshMN2345 changed the title feat: add signup email security controls feat: add signup email security controls + ce refactor changes May 13, 2026
@HarshMN2345
Copy link
Copy Markdown
Member Author

HarshMN2345 commented May 13, 2026

@greptile
listPolicies() always returns the full set of standard project policies. The only fallback needed here is for the new email policies while backend compatibility rolls out. The non-email policy casts are based on the SDK/API contract, not optional data.

.catch() not needed as ik it is used to prevent one failed per-project platform preview fetch from breaking the whole organization overview. Since platforms here are only used for card preview/count display, we can degrade to empty data. If we prefer surfacing regional/API failures instead of hiding them, I can remove the catch and let the page fail as before.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 13, 2026

Both points are well-reasoned. Let me address them in turn:

On listPolicies() fallbacks: Looking at the current code in +page.ts, the approach is correct. There's a single listPolicies() call, the non-email policies are cast directly (justified by the SDK contract guaranteeing they're always present), and only the three new email policies use ?? getDefaultEnabledPolicy(...) for backward compatibility. This is the right amount of defensiveness — no more, no less. My earlier concern was based on a stale read; the current implementation handles this cleanly.

On the platform catch / graceful degradation: The rationale is sound — platforms on the org page are display-only (card preview and count), so degrading to empty data is preferable to surfacing an error that blocks access to the rest of the org. That said, looking at the current +page.ts code I can see, the Promise.all does not yet have a per-project catch:

const projects = await Promise.all(
    activeProjects.projects.map(async (project) => {
        // no catch here — any single failure rejects the whole Promise.all
        const platformList = await sdk
            .forProject(project.region, project.$id)
            .project.listPlatforms(...);
        return { ...project, platforms: platformList.platforms, platformsTotal: platformList.total };
    })
);

If the catch is already in a later commit, disregard. If not, the minimal fix is:

    const projects = await Promise.all(
        activeProjects.projects.map(async (project) => {
            project.region ??= 'default';
            let platforms: Models.Platform[] = [];
            let platformsTotal = 0;
            try {
                const platformList = await sdk
                    .forProject(project.region, project.$id)
                    .project.listPlatforms({ queries: [Query.limit(3)] });
                platforms = platformList.platforms;
                platformsTotal = platformList.total;
            } catch {
                // degrade gracefully — platforms are display-only on this page
            }
            return { ...project, platforms, platformsTotal };
        })
    );

Keeping the error silent (no console.error) is fine given it's purely cosmetic data. The degradation-over-failure approach is the right call here.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@HarshMN2345
Copy link
Copy Markdown
Member Author

@greptile re review
The standard policies from listPolicies() are guaranteed by the SDK/API contract, so no fallback is needed for non-email policies.

Comment on lines +44 to +61
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)
]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 New API calls block the entire project layout on failure

listPlatforms() and listKeys() are now included in the main Promise.all for the project layout. These calls were added purely to support the onboarding progress card. If either call encounters a transient API error, region routing issue, or backend regression, the entire Promise.all rejects — making every project page (dashboards, databases, functions, settings) inaccessible. Wrapping each call in a try/catch (or using Promise.allSettled) and defaulting to empty lists on failure would isolate this risk without changing the onboarding logic.

Comment thread package.json
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "^12.2.0",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7789ae4",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Unpublished SDK pinned by commit hash without integrity verification

@appwrite.io/console has been changed from a stable npm release (^12.2.0) to a bare commit reference on pkg.vc (7789ae4). The corresponding bun.lock entry omits the sha512 integrity hash present for the previous version. If pkg.vc serves different content at this URL — due to a service incident, silent mutation, or supply-chain compromise — nothing in the build process will detect it. This is a core SDK the entire application depends on; it should be pinned to a published, integrity-checked release before this merges to main.

Comment on lines +33 to +54
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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing null fallback for non-email policies can crash the security page

All nine non-email policies are returned from policiesById with a bare type-cast and no fallback (e.g. policiesById[ProjectPolicyId.Sessionduration] as Models.PolicySessionDuration). If the backend omits any of these entries from listPolicies(), the value is undefined at runtime. Child components then access policy.duration, policy.total, and policy.userMFA directly without guards, causing a TypeError that crashes the entire security settings page. The email policies already demonstrate the safe pattern (?? getDefaultEnabledPolicy(...)); the same ?? undefined or a typed optional prop should be applied uniformly to all policy lookups so a partial backend response degrades gracefully.

method: ProjectServiceId.Project,
value: services.get(ProjectServiceId.Project) ?? null
},
// @todo Re-enable when Proxy is ready for public release.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proxy is now ready for public, we can enable

@HarshMN2345 HarshMN2345 merged commit c4f6f79 into main May 14, 2026
4 checks passed
@HarshMN2345 HarshMN2345 deleted the feat-signup-email-security branch May 14, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants