Skip to content

Users add new email attributes #10688

Merged
abnegate merged 23 commits into
1.8.xfrom
users-add-attributes
Nov 12, 2025
Merged

Users add new email attributes #10688
abnegate merged 23 commits into
1.8.xfrom
users-add-attributes

Conversation

@fogelito
Copy link
Copy Markdown
Contributor

What does this PR do?

(Provide a description of what this PR does and why it's needed.)

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. Screenshots may also be helpful.)

Related PRs and Issues

  • (Related PR or issue)

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

@fogelito fogelito changed the base branch from main to 1.8.x October 23, 2025 09:39
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Oct 23, 2025

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
binutils 2.44-r2 CVE-2025-5244 HIGH
binutils 2.44-r2 CVE-2025-5245 HIGH
libxml2 2.13.8-r0 CVE-2025-49794 CRITICAL
libxml2 2.13.8-r0 CVE-2025-49796 CRITICAL
libxml2 2.13.8-r0 CVE-2025-49795 HIGH
libxml2 2.13.8-r0 CVE-2025-6021 HIGH
pcre2 10.43-r1 CVE-2025-58050 CRITICAL
github.com/containerd/containerd/v2 v2.0.2 CVE-2024-25621 HIGH
golang.org/x/crypto v0.31.0 CVE-2025-22869 HIGH
golang.org/x/oauth2 v0.24.0 CVE-2025-22868 HIGH
stdlib 1.22.10 CVE-2025-47907 HIGH
stdlib 1.22.10 CVE-2025-47912 HIGH
stdlib 1.22.10 CVE-2025-58183 HIGH
stdlib 1.22.10 CVE-2025-58186 HIGH
stdlib 1.22.10 CVE-2025-58187 HIGH
stdlib 1.22.10 CVE-2025-58188 HIGH
stdlib 1.22.10 CVE-2025-61724 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Oct 23, 2025

✨ Benchmark results

  • Requests per second: 1,370
  • Requests with 200 status code: 246,563
  • P99 latency: 0.14560122

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,370 1,473
200 246,563 265,114
P99 0.14560122 0.138999128

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 27, 2025

📝 Walkthrough

Walkthrough

This PR adds five new public user attributes to the Users collection: emailCanonical (string) and four booleans — emailIsFree, emailIsDisposable, emailIsCorporate, emailIsCanonical. Controllers (account, teams, users) switch parameter validation to Appwrite\Network\Validator\Email aliased as EmailValidator, and attempt to canonicalize provided emails using Utopia\Emails\Email inside try/catch blocks. The canonical string and related boolean flags are attached to created/updated user documents, tokens, sessions, and membership/invite flows. An end-to-end test (tests/e2e/Services/Account/AccountBase.php) was modified to dump a response body and includes an always-failing assertion.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • app/config/collections/common.php: five homogeneous schema additions — quick validation of types, sizes, defaults.
  • app/controllers/api/account.php: multiple authentication, token, session, and user-returning flows updated to compute and propagate canonical email metadata — requires walkthrough across distinct flows.
  • app/controllers/api/teams.php: membership/invite creation refactored to create users with canonical fields and propagate them into membership flows.
  • app/controllers/api/users.php: user creation and email-update paths extended to compute/store canonical metadata; several endpoints switched to EmailValidator.
  • tests/e2e/Services/Account/AccountBase.php: debug output added and an always-failing assertion introduced — will break CI and needs urgent fix.

Areas needing extra attention:

  • Remove or revert the debug dump and failing assertion in tests.
  • Verify consistent null handling and exception swallowing around Utopia\Emails\Email instantiation across all try/catch sites.
  • Confirm all public API responses intentionally and consistently expose the new fields.
  • Check whether adding public attributes requires index adjustments, migration steps, or changes to API docs.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The PR description contains only the repository's contribution template with no substantive content about the changes, motivation, testing, or related issues. Fill in the PR description template with details about what the new email attributes are, why they were added, how to test the changes, and any related issues or PRs.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding new email attributes to the Users collection, which matches the core modifications across all affected files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch users-add-attributes

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b4a56c and 97454c0.

📒 Files selected for processing (1)
  • app/controllers/api/users.php (16 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/users.php (1)
src/Appwrite/GraphQL/Types/Mapper.php (1)
  • param (255-363)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan
🔇 Additional comments (5)
app/controllers/api/users.php (5)

19-19: Import aliasing improves code clarity.

The aliasing of Email as EmailValidator for the validator class effectively prevents naming conflicts with Utopia\Emails\Email (the canonicalization class) and makes the code intent clearer.

Also applies to: 52-52


101-105: Good exception handling for email canonicalization.

The try-catch block correctly handles instantiation failures by falling back to null, ensuring user creation doesn't fail if canonicalization isn't possible.


223-223: Validator updates are consistent across user creation endpoints.

All user creation endpoints now consistently use EmailValidator for email parameter validation, which aligns with the import aliasing and improves code clarity.

Also applies to: 258-258, 293-293, 328-328, 363-363, 405-405, 440-440, 488-488


542-542: Target validation updated consistently.

Email validation for target creation and update now uses EmailValidator, maintaining consistency with user creation endpoints.

Also applies to: 1726-1726


1417-1417: Email update validation and canonicalization implemented correctly.

The email parameter correctly allows empty values for the update endpoint, and the canonicalization follows the same safe try-catch pattern used in user creation.

Also applies to: 1452-1456


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4f63ed and 218bb4c.

📒 Files selected for processing (1)
  • app/config/collections/common.php (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan

Comment thread app/config/collections/common.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/controllers/api/account.php (2)

379-405: Missing canonical email attributes in account creation.

The POST /v1/account endpoint creates users with email (line 386) but doesn't set the canonical email attributes that are being added to the schema. This is inconsistent with the email token creation flow (lines 2231-2260) which does set them.

Impact: Users created via email/password won't have email canonicalization metadata, leading to:

  • Incomplete data for analytics/filtering on email types
  • Inconsistent user records across different registration methods

Apply this diff to add canonical email attributes:

+        $emailCanonical = new EmailCanonical($email);
         try {
             $userId = $userId == 'unique()' ? ID::unique() : $userId;
             $user->setAttributes([
                 '$id' => $userId,
                 '$permissions' => [
                     Permission::read(Role::any()),
                     Permission::update(Role::user($userId)),
                     Permission::delete(Role::user($userId)),
                 ],
                 'email' => $email,
                 'emailVerification' => false,
+                'emailCanonical' => $emailCanonical->getCanonical(),
+                'emailIsCanonical' => $emailCanonical->isCanonicalSupported(),
+                'emailIsCorporate' => $emailCanonical->isCorporate(),
+                'emailIsDisposable' => $emailCanonical->isDisposable(),
+                'emailIsFree' => $emailCanonical->isFree(),
                 'status' => true,
                 'password' => $password,

1981-2005: Missing canonical email attributes in magic URL user creation.

When creating a new user via the magic URL token flow, email is set (line 1988) but canonical email attributes are not. This is the same inconsistency found in the account creation endpoint.

Impact: Users created via magic URL won't have email canonicalization metadata.

Apply this diff:

             $userId = $userId === 'unique()' ? ID::unique() : $userId;

+            $emailCanonical = new EmailCanonical($email);
             $user->setAttributes([
                 '$id' => $userId,
                 '$permissions' => [
                     Permission::read(Role::any()),
                     Permission::update(Role::user($userId)),
                     Permission::delete(Role::user($userId)),
                 ],
                 'email' => $email,
                 'emailVerification' => false,
+                'emailCanonical' => $emailCanonical->getCanonical(),
+                'emailIsCanonical' => $emailCanonical->isCanonicalSupported(),
+                'emailIsCorporate' => $emailCanonical->isCorporate(),
+                'emailIsDisposable' => $emailCanonical->isDisposable(),
+                'emailIsFree' => $emailCanonical->isFree(),
                 'status' => true,
                 'password' => null,
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 037d606 and e40e88c.

📒 Files selected for processing (1)
  • app/controllers/api/account.php (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/account.php (1)
src/Appwrite/Auth/OAuth2/Oidc.php (1)
  • getUserEmail (130-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan
🔇 Additional comments (2)
app/controllers/api/account.php (2)

60-60: LGTM on the import statement.

The import and alias are clear and appropriate for the email canonicalization functionality.


2231-2260: LGTM! Clean implementation of canonical email attributes.

This code correctly:

  • Creates the EmailCanonical object once from the validated email
  • Sets all five canonical attributes during user creation
  • Leverages the existing email validation from the param validator

Comment thread app/controllers/api/account.php
Comment thread app/controllers/api/account.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
app/controllers/api/teams.php (1)

606-610: Duplicate issue: unhandled exceptions from EmailCanonical methods.

The same TODO comments and potential exception handling issues exist here as in users.php (lines 134-138). The isDisposable() and isFree() methods may throw exceptions that are not caught.

This affects the team invitation flow when creating new users. Please address this consistently across both files.

app/controllers/api/account.php (1)

1594-1621: Critical: OAuth2 new users still missing canonical email attributes.

This is the same critical issue flagged in the previous review. When new OAuth2 users are created with an email on line 1603, the canonical email attributes (emailCanonical, emailIsCanonical, emailIsCorporate, emailIsDisposable, emailIsFree) are not set. The subsequent check at line 1690 for empty($user->getAttribute('email')) will be false for these newly created users, so that block is skipped and canonical attributes are never populated.

Apply this fix to add canonical email attributes during OAuth2 user creation:

                 $identityWithMatchingEmail = $dbForProject->findOne('identities', [
                     Query::equal('providerEmail', [$email]),
                 ]);
                 if (!$identityWithMatchingEmail->isEmpty()) {
                     $failureRedirect(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
                 }

+                try {
+                    $emailCanonical = new EmailCanonical($email);
+                } catch (Throwable) {
+                    $emailCanonical = null;
+                }
+
                 try {
                     $userId = ID::unique();
                     $user->setAttributes([
                         '$id' => $userId,
                         '$permissions' => [
                             Permission::read(Role::any()),
                             Permission::update(Role::user($userId)),
                             Permission::delete(Role::user($userId)),
                         ],
                         'email' => $email,
                         'emailVerification' => true,
                         'status' => true, // Email should already be authenticated by OAuth2 provider
                         'password' => null,
                         'hash' => Auth::DEFAULT_ALGO,
                         'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
                         'passwordUpdate' => null,
                         'registration' => DateTime::now(),
                         'reset' => false,
                         'name' => $name,
                         'mfa' => false,
                         'prefs' => new \stdClass(),
                         'sessions' => null,
                         'tokens' => null,
                         'memberships' => null,
                         'authenticators' => null,
                         'search' => implode(' ', [$userId, $email, $name]),
                         'accessedAt' => DateTime::now(),
+                        'emailCanonical' => $emailCanonical?->getCanonical(),
+                        'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
+                        'emailIsCorporate' => $emailCanonical?->isCorporate(),
+                        'emailIsDisposable' => $emailCanonical?->isDisposable(),
+                        'emailIsFree' => $emailCanonical?->isFree(),
                     ]);
🧹 Nitpick comments (1)
app/controllers/api/account.php (1)

1690-1703: Inconsistent error handling: silent failure.

While this code sets canonical attributes for OAuth2 users without email, it uses a different error handling pattern compared to account creation (lines 378-382) and email token creation (lines 2247-2251). The empty catch block silently suppresses any exceptions, making debugging difficult if the EmailCanonical library throws unexpected errors.

Consider using the same pattern as lines 378-382 for consistency:

         if (empty($user->getAttribute('email'))) {
             $user->setAttribute('email', $oauth2->getUserEmail($accessToken));

             try {
                 $emailCanonical = new EmailCanonical($user->getAttribute('email'));
-
-                $user->setAttribute('emailCanonical', $emailCanonical->getCanonical());
-                $user->setAttribute('emailIsCanonical', $emailCanonical->isCanonicalSupported());
-                $user->setAttribute('emailIsCorporate', $emailCanonical->isCorporate());
-                $user->setAttribute('emailIsDisposable', $emailCanonical->isDisposable());
-                $user->setAttribute('emailIsFree', $emailCanonical->isFree());
             } catch (Throwable) {
+                $emailCanonical = null;
             }
+
+            $user
+                ->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
+                ->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
+                ->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
+                ->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
+                ->setAttribute('emailIsFree', $emailCanonical?->isFree());
         }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca8cb6f and 371cab0.

📒 Files selected for processing (3)
  • app/controllers/api/account.php (7 hunks)
  • app/controllers/api/teams.php (3 hunks)
  • app/controllers/api/users.php (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: scan
🔇 Additional comments (6)
app/controllers/api/users.php (1)

52-52: LGTM!

The import statement correctly aliases the Utopia\Emails\Email class as EmailCanonical for use in email canonicalization.

app/controllers/api/teams.php (2)

51-51: LGTM!

The import statement is consistent with the same change in users.php.


568-572: No action required—the exception handling is already in place.

The original concern states that "the EmailCanonical constructor may throw an exception that would not be caught," but this is contradicted by the code: lines 569–572 explicitly wrap the instantiation in a try-catch block that catches Throwable exceptions.

The code flow confirms that $email can indeed be empty (when a phone-only invitation is sent and no existing user is found), and external Email classes do throw exceptions on empty addresses. However, the defensive try-catch pattern here is intentional—the exception is caught, and $emailCanonical is safely set to null if validation fails. This aligns with how the code later handles the result at line 584: 'email' => empty($email) ? null : $email.

Likely an incorrect or invalid review comment.

app/controllers/api/account.php (3)

60-60: LGTM!

The import alias is well-chosen and follows clear naming conventions.


378-417: Good defensive error handling for canonical email attributes.

The try-catch block with null fallback and null-safe operators ensures account creation proceeds even if canonical email computation fails. This is appropriate for a non-critical enhancement.


2247-2280: LGTM!

The error handling pattern here is consistent with account creation (lines 378-382): try-catch with null fallback and null-safe operators for attribute setting. This ensures user creation succeeds even if canonical email computation fails.

Comment thread app/controllers/api/account.php Outdated
Comment thread app/controllers/api/users.php
Comment thread app/controllers/api/users.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/controllers/api/teams.php (1)

569-611: Consider extracting email canonicalization logic into a helper function.

This email canonicalization pattern (constructing EmailCanonical, handling errors, and populating user document fields) appears to be repeated across multiple controllers (account.php, users.php, and teams.php per the AI summary).

Consider extracting this into a shared helper method to improve maintainability and ensure consistency.

Example structure:

// In a helper class (e.g., src/Appwrite/Auth/EmailHelper.php)
public static function getCanonicalEmailFields(?string $email): array
{
    if (empty($email)) {
        return [
            'emailCanonical' => null,
            'emailIsCanonical' => null,
            'emailIsCorporate' => null,
            'emailIsDisposable' => null,
            'emailIsFree' => null,
        ];
    }

    try {
        $emailCanonical = new EmailCanonical($email);
        return [
            'emailCanonical' => $emailCanonical->getCanonical(),
            'emailIsCanonical' => $emailCanonical->isCanonicalSupported(),
            'emailIsCorporate' => $emailCanonical->isCorporate(),
            'emailIsDisposable' => $emailCanonical->isDisposable(),
            'emailIsFree' => $emailCanonical->isFree(),
        ];
    } catch (SpecificException $e) {
        // Log the error for debugging
        return [
            'emailCanonical' => null,
            'emailIsCanonical' => null,
            'emailIsCorporate' => null,
            'emailIsDisposable' => null,
            'emailIsFree' => null,
        ];
    }
}

Then use it consistently across all controllers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 371cab0 and 5031457.

📒 Files selected for processing (1)
  • app/controllers/api/teams.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/teams.php (1)
src/Appwrite/Auth/Auth.php (3)
  • Auth (18-515)
  • passwordHash (220-257)
  • passwordGenerator (317-320)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: scan
🔇 Additional comments (2)
app/controllers/api/teams.php (2)

51-51: LGTM! Clean import alias.

The EmailCanonical alias clearly distinguishes the email canonicalization utility from the validator imported on line 13.


609-610: Clarify and handle exceptions from EmailCanonical::isDisposable()/::isFree()

Location: app/controllers/api/teams.php: lines 609–610.

  • Null-safe operator ($emailCanonical?->...) only guards against null; it does not catch exceptions — the "todo: fix throw" implies these methods may throw.
  • Repo search did not locate isDisposable()/isFree() implementations — confirm whether they throw. If they can throw, either wrap these calls in try/catch and return a safe default (e.g., false/null) or add a non-throwing accessor on EmailCanonical.
  • Standardize the comment to "TODO: ..." for consistency.

Comment thread app/controllers/api/teams.php Outdated
Comment thread app/controllers/api/teams.php
@fogelito fogelito changed the title Users add new attributes Users add new email attributes Nov 2, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a41f8b and fe9d49c.

📒 Files selected for processing (2)
  • src/Appwrite/Utopia/Response/Model/User.php (1 hunks)
  • tests/e2e/Services/Account/AccountBase.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Appwrite/Utopia/Response/Model/User.php (1)
src/Appwrite/Utopia/Response/Model.php (1)
  • addRule (90-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: scan

Comment thread src/Appwrite/Utopia/Response/Model/User.php Outdated
Comment thread tests/e2e/Services/Account/AccountBase.php Outdated
Comment thread tests/e2e/Services/Account/AccountBase.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
app/controllers/api/teams.php (1)

570-574: Validate email before canonicalization to prevent empty string instantiation.

The $email variable can be an empty string at this point in the flow (e.g., line 537 assigns $invitee->getAttribute('email', '')). Passing an empty string to EmailCanonical may not be appropriate. Consider adding an empty check:

-            try {
-                $emailCanonical = new EmailCanonical($email);
-            } catch (Throwable) {
-                $emailCanonical = null;
-            }
+            $emailCanonical = null;
+            if (!empty($email)) {
+                try {
+                    $emailCanonical = new EmailCanonical($email);
+                } catch (Throwable) {
+                    $emailCanonical = null;
+                }
+            }

Based on past review analysis.

app/controllers/api/account.php (2)

1711-1724: Critical: OAuth2 new users still missing canonical email attributes.

The previous review comment on these lines has NOT been addressed. New OAuth2 users created at lines 1615-1659 are instantiated with an email (line 1624), so when execution reaches line 1711, the condition if (empty($user->getAttribute('email'))) evaluates to false and this entire block is skipped. As a result, new OAuth2 users will NOT have canonical email attributes set.

The fix should add canonical attribute setting in the new user creation block (around lines 1615-1642) where the email is first assigned.


3111-3120: Major: Email update still missing error handling for EmailCanonical.

The previous review comment on lines 3111-3121 has NOT been addressed. Unlike account creation (lines 399-403) and email token creation (lines 2268-2272), this email update path does not wrap EmailCanonical instantiation in a try-catch block. If the EmailCanonical library throws an exception for an edge-case email format, the user's email update will fail entirely—degrading user experience for what should be a non-critical metadata enhancement.

Wrap the instantiation in try-catch and use null-safe operators when setting attributes, consistent with the other email flows.

🧹 Nitpick comments (2)
app/controllers/api/users.php (1)

101-105: Guard EmailCanonical instantiation with an empty check instead of null coalescing.

While the null coalescing operator prevents passing null to EmailCanonical, passing an empty string when $email is absent may not be semantically correct. Consider checking if the email is non-empty before instantiation:

Apply this pattern to avoid unnecessary instantiation:

-        try {
-            $emailCanonical = new EmailCanonical($email ?? '');
-        } catch (Throwable) {
-            $emailCanonical = null;
-        }
+        $emailCanonical = null;
+        if (!empty($email)) {
+            try {
+                $emailCanonical = new EmailCanonical($email);
+            } catch (Throwable) {
+                $emailCanonical = null;
+            }
+        }
app/controllers/api/teams.php (1)

590-590: Remove or resolve the TODO comment about password handling.

The TODO at line 590 suggests uncertainty about the password handling approach. However, the comment at lines 594-598 clearly explains that the random password is intentional to allow password updates without requiring an old password. Either:

  1. Remove the TODO if the current approach is finalized, or
  2. Document the decision more clearly

If the current approach is correct, apply this diff:

-                // TODO: Set password empty?
                 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),

As per past review comments.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe9d49c and 5afae24.

📒 Files selected for processing (3)
  • app/controllers/api/account.php (7 hunks)
  • app/controllers/api/teams.php (2 hunks)
  • app/controllers/api/users.php (3 hunks)
🔇 Additional comments (4)
app/controllers/api/users.php (1)

134-138: Verify exception handling for EmailCanonical method calls.

Previous reviews indicated that isDisposable() and isFree() can throw exceptions when data files are unavailable. While marked as addressed in commit 5031457, the current code only wraps the constructor in a try-catch (lines 101-105). The null-safe operator (?->) prevents calls on null but doesn't catch exceptions thrown during method execution.

Please confirm whether:

  1. These methods have been updated to never throw exceptions, or
  2. Additional exception handling is needed around these method calls

If exceptions can still occur, wrap each attribute assignment in exception handling:

-            'emailCanonical' => $emailCanonical?->getCanonical(),
-            'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
-            'emailIsCorporate' => $emailCanonical?->isCorporate(),
-            'emailIsDisposable' => $emailCanonical?->isDisposable(),
-            'emailIsFree' => $emailCanonical?->isFree(),
+            'emailCanonical' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->getCanonical(); } catch (Throwable) { return null; }
+            })() : null,
+            'emailIsCanonical' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isCanonicalSupported(); } catch (Throwable) { return false; }
+            })() : false,
+            'emailIsCorporate' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isCorporate(); } catch (Throwable) { return false; }
+            })() : false,
+            'emailIsDisposable' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isDisposable(); } catch (Throwable) { return false; }
+            })() : false,
+            'emailIsFree' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isFree(); } catch (Throwable) { return false; }
+            })() : false,

Based on past review analysis.

app/controllers/api/teams.php (1)

608-612: Verify exception handling for EmailCanonical method calls.

Similar to users.php, these method calls use the null-safe operator but may still throw exceptions during execution. Previous reviews indicated that isDisposable() and isFree() can throw when data files are unavailable.

If these methods can throw exceptions, consider wrapping each call in exception handling. See the pattern suggested for users.php lines 134-138.

Based on past review analysis.

app/controllers/api/account.php (2)

399-403: LGTM: Proper error handling in account creation.

The try-catch block ensures that EmailCanonical instantiation failures won't break account creation, and the null-safe operator correctly handles the null case when setting attributes.

Also applies to: 433-437


2268-2272: LGTM: Consistent error handling in email token creation.

The implementation matches the account creation flow with proper try-catch and null-safe operators, ensuring email token creation doesn't fail if canonical email processing encounters an error.

Also applies to: 2296-2300

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
app/controllers/api/account.php (1)

1723-1736: Optimize: Reuse existing email variable.

Line 1724 calls $oauth2->getUserEmail($accessToken) but the $email variable is already available from Line 1521. Consider reusing it for consistency and to avoid redundant OAuth2 provider calls.

Apply this optimization:

-        if (empty($user->getAttribute('email'))) {
-            $user->setAttribute('email', $oauth2->getUserEmail($accessToken));
+        if (empty($user->getAttribute('email'))) {
+            $user->setAttribute('email', $email);
 
             try {
-                $emailCanonical = new EmailCanonical($user->getAttribute('email'));
+                $emailCanonical = new EmailCanonical($email);
             } catch (Throwable) {
                 $emailCanonical = null;
             }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6096c42 and b28e536.

📒 Files selected for processing (1)
  • app/controllers/api/account.php (12 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Linter
  • GitHub Check: CodeQL
  • GitHub Check: scan
🔇 Additional comments (7)
app/controllers/api/account.php (7)

60-60: LGTM! Clear aliasing for email canonicalization utility.

The import statement clearly indicates the purpose of the utility through the alias.


398-403: LGTM! Proper error handling for email canonicalization in account creation.

The try-catch block prevents user creation failures if the EmailCanonical library throws an exception, and the nullsafe operators ensure graceful degradation.

Also applies to: 433-437


1615-1619: LGTM! Critical OAuth2 issue from past reviews has been addressed.

New OAuth2 users now receive canonical email attributes during account creation. Previously, the canonical attributes were only set in a conditional block that was skipped for newly created users.

Also applies to: 1648-1652


2031-2035: LGTM! Consistent error handling for magic URL flow.

Email canonicalization is properly handled with try-catch and nullsafe operators, consistent with other authentication flows.

Also applies to: 2061-2065


2292-2296: LGTM! Consistent error handling for email token flow.

Email canonicalization follows the same reliable pattern used throughout the file.

Also applies to: 2320-2324


2672-2676: LGTM! Correct handling for phone-only users.

Setting email canonical attributes to null for phone authentication is the appropriate approach, as these users don't have an email address.


3140-3144: LGTM! Email update now includes proper error handling.

The try-catch block addresses the concern from previous reviews, ensuring that email updates don't fail if the EmailCanonical library encounters an edge-case email format.

Also applies to: 3149-3153

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/controllers/api/users.php (1)

1461-1465: Duplicate: Verify exception safety of EmailCanonical methods.

Same concern as lines 134-138—if isDisposable() and isFree() can throw exceptions, the user update operation will fail. Please verify these methods are exception-safe or wrap the calls appropriately.

🧹 Nitpick comments (2)
app/controllers/api/users.php (2)

101-105: Consider guarding against empty email before instantiation.

The try-catch handles exceptions gracefully, but since $email can be null or empty at this point, it's clearer to check before attempting instantiation:

+if (!empty($email)) {
     try {
         $emailCanonical = new EmailCanonical($email);
     } catch (Throwable) {
         $emailCanonical = null;
     }
+} else {
+    $emailCanonical = null;
+}

This makes the intent explicit and avoids relying on exception handling for expected conditions.


1452-1456: Consider guarding against empty email before instantiation.

Similar to lines 101-105, the email could be an empty string at this point (as checked by the strlen($email) !== 0 condition above). Consider checking before instantiation:

+if (\strlen($email) !== 0) {
     try {
         $emailCanonical = new EmailCanonical($email);
     } catch (Throwable) {
         $emailCanonical = null;
     }
+} else {
+    $emailCanonical = null;
+}

This avoids relying on exception handling for expected empty-email scenarios.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a58f7b and 27d0c51.

📒 Files selected for processing (1)
  • app/controllers/api/users.php (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Benchmark
  • GitHub Check: scan
🔇 Additional comments (1)
app/controllers/api/users.php (1)

52-52: LGTM: Clean import aliasing.

The EmailCanonical alias is clear and aligns with the usage pattern across other controllers.

Comment thread app/controllers/api/users.php
Comment thread app/controllers/api/account.php Outdated
@fogelito fogelito requested a review from abnegate November 11, 2025 13:05
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
app/controllers/api/teams.php (2)

590-590: [Duplicate] Remove or clarify TODO comment about password handling.

This TODO was previously flagged. The code intentionally generates a random password (lines 591-593) to allow team-invited users to set a password later without providing an old password (explained in the comment at lines 594-598).

Either remove the TODO if the current approach is correct, or clarify the intended behavior.

Suggested clarification:

-                // TODO: Set password empty?
+                // Random password generated for team-invited users; allows password updates without old password
                 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),

570-574: [Duplicate] Empty email validation missing and unprotected method calls can throw exceptions.

Two issues previously flagged but still present:

  1. Empty email not validated before canonicalization: At line 570, $email can be an empty string (e.g., line 536: $email = $invitee->getAttribute('email', '');). Attempting to canonicalize an empty string is unnecessary and may cause unexpected behavior.

  2. Unprotected method calls: Similar to users.php, the constructor at lines 570-574 is protected, but method calls at lines 608-612 are not. If isDisposable() or isFree() throw exceptions, user creation will fail.

Apply these fixes:

+        if (!empty($email)) {
             try {
                 $emailCanonical = new Email($email);
             } catch (Throwable) {
                 $emailCanonical = null;
             }
+        } else {
+            $emailCanonical = null;
+        }

And wrap method calls similarly to the suggestion for users.php:

-            'emailCanonical' => $emailCanonical?->getCanonical(),
-            'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
-            'emailIsCorporate' => $emailCanonical?->isCorporate(),
-            'emailIsDisposable' => $emailCanonical?->isDisposable(),
-            'emailIsFree' => $emailCanonical?->isFree(),
+            'emailCanonical' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->getCanonical(); } catch (Throwable) { return null; }
+            })() : null,
+            'emailIsCanonical' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isCanonicalSupported(); } catch (Throwable) { return null; }
+            })() : null,
+            'emailIsCorporate' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isCorporate(); } catch (Throwable) { return false; }
+            })() : false,
+            'emailIsDisposable' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isDisposable(); } catch (Throwable) { return false; }
+            })() : false,
+            'emailIsFree' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isFree(); } catch (Throwable) { return false; }
+            })() : false,

Also applies to: 608-612

app/controllers/api/users.php (2)

101-105: [Duplicate] Unprotected method calls can throw exceptions despite constructor protection.

Past reviews identified that isDisposable() and isFree() methods can throw exceptions when data files are invalid. While the constructor at lines 101-105 is wrapped in try-catch, the method calls at lines 134-138 remain unprotected. The null-safe operator (?->) prevents null pointer exceptions but does not catch exceptions thrown during method execution, which will crash user creation operations.

This was previously flagged and marked as addressed, but the method calls still lack exception handling.

Wrap each method call in try-catch:

-            'emailCanonical' => $emailCanonical?->getCanonical(),
-            'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
-            'emailIsCorporate' => $emailCanonical?->isCorporate(),
-            'emailIsDisposable' => $emailCanonical?->isDisposable(),
-            'emailIsFree' => $emailCanonical?->isFree(),
+            'emailCanonical' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->getCanonical(); } catch (Throwable) { return null; }
+            })() : null,
+            'emailIsCanonical' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isCanonicalSupported(); } catch (Throwable) { return null; }
+            })() : null,
+            'emailIsCorporate' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isCorporate(); } catch (Throwable) { return false; }
+            })() : false,
+            'emailIsDisposable' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isDisposable(); } catch (Throwable) { return false; }
+            })() : false,
+            'emailIsFree' => $emailCanonical ? (function() use ($emailCanonical) {
+                try { return $emailCanonical->isFree(); } catch (Throwable) { return false; }
+            })() : false,

Also applies to: 134-138


1461-1465: [Duplicate] Unprotected method calls in email update path can throw exceptions.

Similar to the user creation path, the email update path wraps the constructor (lines 1452-1456) but leaves method calls (lines 1461-1465) unprotected. If isDisposable() or isFree() throw exceptions, the email update operation will fail.

Additionally, this is compounded by the bug at line 1453 where the wrong class is being instantiated.

Apply the same protection pattern as suggested for lines 134-138, wrapping each method call in try-catch blocks with safe fallback values.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da0a6b1 and 8b4a56c.

📒 Files selected for processing (3)
  • app/controllers/api/account.php (19 hunks)
  • app/controllers/api/teams.php (4 hunks)
  • app/controllers/api/users.php (16 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-23T08:06:38.889Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10546
File: src/Appwrite/Platform/Workers/Migrations.php:144-148
Timestamp: 2025-10-23T08:06:38.889Z
Learning: In the Appwrite codebase, migration workers receive already-validated data from queued jobs. Query validation using Query::parseQueries() happens at the API endpoint level (with try-catch for QueryException) before jobs are queued, so workers in src/Appwrite/Platform/Workers/Migrations.php don't need redundant validation.

Applied to files:

  • app/controllers/api/teams.php
🧬 Code graph analysis (3)
app/controllers/api/users.php (1)
src/Appwrite/GraphQL/Types/Mapper.php (1)
  • param (255-363)
app/controllers/api/account.php (1)
src/Appwrite/GraphQL/Types/Mapper.php (1)
  • param (255-363)
app/controllers/api/teams.php (1)
src/Appwrite/GraphQL/Types/Mapper.php (1)
  • param (255-363)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: scan
🔇 Additional comments (11)
app/controllers/api/users.php (1)

19-19: LGTM! Import alias and validator usage correctly updated.

The changes properly separate concerns:

  • EmailValidator alias for Appwrite\Network\Validator\Email used for parameter validation
  • Email from Utopia\Emails\Email used for canonicalization logic

All parameter declarations and inline validator instantiations correctly use EmailValidator().

Also applies to: 52-52, 223-223, 258-258, 293-293, 328-328, 363-363, 405-405, 440-440, 488-488, 542-542, 1417-1417, 1726-1726

app/controllers/api/teams.php (2)

13-13: LGTM! Import alias and validator usage correctly implemented.

The changes properly separate the validator (EmailValidator) from the canonicalization class (Email), consistent with the pattern in users.php.

Also applies to: 51-51, 472-472


576-616: Well-structured user document creation for team invitations.

The userDocument includes all necessary fields and properly uses Authorization::skip for the team invitation flow. The structure is consistent with the createUser function in users.php, including the canonical email attributes.

app/controllers/api/account.php (8)

23-23: LGTM! Import aliases follow review feedback.

The import structure properly separates validator and canonicalization concerns:

  • EmailValidator for parameter validation
  • Email (Utopia\Emails) for canonicalization logic

Also applies to: 60-60


341-341: LGTM! Consistent parameter validation.

All email parameter declarations consistently use EmailValidator for validation.

Also applies to: 920-920, 1985-1985, 2249-2249, 3105-3105, 3390-3390


399-437: LGTM! Robust error handling in account creation.

The canonicalization flow properly handles failures:

  • Try-catch prevents exceptions from aborting user creation
  • Null-safe operators gracefully handle failed canonicalization
  • Email is normalized to lowercase before processing

1615-1653: LGTM! OAuth2 canonical attributes properly set for new users.

The OAuth2 flow correctly sets canonical attributes during new user creation (lines 1648-1652), addressing the critical issue from previous reviews. New users are created with both email and canonical attributes in a single operation.


1723-1737: LGTM! OAuth2 existing user email update handles canonicalization correctly.

The flow for existing OAuth2 users with empty email properly:

  1. Sets email from OAuth provider
  2. Attempts canonicalization with error handling
  3. Sets canonical attributes using null-safe operators

2031-2065: LGTM! Token creation flows handle canonicalization consistently.

Both Magic URL and Email OTP token creation flows:

  • Wrap Email instantiation in try-catch blocks
  • Use null-safe operators for canonical attributes
  • Properly handle user creation with canonical data

Also applies to: 2292-2324


2672-2676: LGTM! Phone-based user creation correctly handles missing email.

Phone authentication users appropriately have all email canonical attributes set to null since they don't have an email address.


3140-3153: LGTM! Email update flow properly refreshes canonical attributes.

The email update endpoint:

  • Wraps canonicalization in try-catch for resilience
  • Uses null-safe operators to handle failures gracefully
  • Normalizes email to lowercase before processing
  • Correctly resets email verification after update

Comment thread app/controllers/api/users.php
@abnegate abnegate merged commit b46ebb9 into 1.8.x Nov 12, 2025
72 of 74 checks passed
@abnegate abnegate deleted the users-add-attributes branch November 12, 2025 03:19
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