Skip to content

Conversation

@Alllex202
Copy link
Contributor

@Alllex202 Alllex202 commented Nov 26, 2025

Demo

2026-01-20.19.26.33.mov
2026-01-20.19.24.44.mov

Summary by CodeRabbit

  • New Features

    • Expanded two‑factor authentication across sign‑in, registration, change‑password and profile flows with selectable second factors, code entry UI, problem reporting, sudo/password modals, and a mutation to toggle 2FA.
  • Privacy

    • Masked display of emails and phones in prompts and error details; improved phone/email formatting and masking for international numbers.
  • Localization

    • Added 2FA translations for English, Spanish and Russian.
  • Style

    • Form labels now wrap to multiple lines.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end two‑factor authentication (2FA): new GraphQL types/mutations and errors, server-side 2FA-aware credential validation, frontend 2FA hooks/components and modal flows, masking utilities/tests, schema/client updates, and localization/UI tweaks.

Changes

Cohort / File(s) Summary
Masking & Normalization Utilities
apps/condo/domains/common/utils/mail.js, apps/condo/domains/common/utils/mail.spec.js, apps/condo/domains/common/utils/phone.js, apps/condo/domains/common/utils/phone.spec.js, apps/condo/domains/common/utils/helpers.ts
Added maskNormalizedEmail and maskNormalizedPhone with tests; extended formatPhone to accept isMaskedPhone and use masked regex paths.
Regex Constants
apps/condo/domains/common/constants/regexps.js
Added and exported NOT_NUMBER_REGEX.
2FA Constants & Errors
apps/condo/domains/user/constants/authFactors.js, apps/condo/domains/user/constants/errors.js
Added AUTH_FACTOR_TYPES and NOT_ENOUGH_AUTH_FACTORS constants and exports.
GraphQL Types & Client Hooks
apps/condo/schema.graphql, apps/condo/schema.ts, apps/condo/gql/index.ts, apps/condo/gql/operation.types.ts, apps/condo/domains/user/queries/ChangeTwoFactorAuthentication.graphql, apps/condo/domains/user/queries/User.graphql
Introduced second-factor enums/inputs on auth mutations, added ChangeTwoFactorAuthentication mutation and client hook/types, and exposed isPhoneVerified / isTwoFactorAuthenticationEnabled on user selection.
Server Auth Services (Resolvers)
apps/condo/domains/user/schema/*Service.js (Authenticate*, GenerateSudoTokenService, Confirm*ActionService, etc.)
Resolvers accept/process secondFactor, may throw/enrich NOT_ENOUGH_AUTH_FACTORS with availableSecondFactors, maskedData, userId; startConfirm* actions accept either identifier or user.
Server Auth Utilities & Tests
apps/condo/domains/user/utils/serverSchema/auth.js, apps/condo/domains/user/utils/serverSchema/auth.spec.js
Exported ERROR_TYPES; _matchUser returns richer 2FA error payloads, uses masking utils; tests updated for new payload shape and token-derived data.
Test Helpers & Tests
apps/condo/domains/user/utils/testSchema/index.js, apps/condo/domains/user/schema/*.test.js
Added test helpers for confirm actions, start/complete helpers, and numerous 2FA tests (positive/negative) verifying masking and flow.
Frontend 2FA Components & Hooks
apps/condo/domains/user/components/auth/SecondFactorForm.tsx, apps/condo/domains/user/components/auth/SignInForm.tsx, apps/condo/domains/user/components/auth/RegisterForm.tsx, apps/condo/pages/auth/change-password.tsx, apps/condo/domains/user/components/auth/ValidateIdentifierForm.tsx
Added useSecondFactor hook, SecondFactor UI and integrated 2FA flows into sign-in, registration, change-password and validation flows; updated labels and validation messaging.
Sudo Token & User Settings
apps/condo/domains/user/components/SudoTokenProvider.tsx, apps/condo/domains/user/components/UserSettingsContent.tsx
Extended getSudoTokenWithModal signature (optional labels), added 2FA modal flows when NOT_ENOUGH_AUTH_FACTORS occurs, wired changeTwoFactorAuthentication mutation and token handling.
Client Types & Pages
apps/condo/gql/operation.types.ts, apps/condo/pages/auth/signin.tsx
Added client mutation types for ChangeTwoFactorAuthentication and adjusted signin routing/step gating for 2FA step.
Localization
apps/condo/lang/en/en.json, apps/condo/lang/es/es.json, apps/condo/lang/ru/ru.json
Added translations for SecondFactorForm, SudoTokenProvider modal, profile 2FA labels; added API error keys; removed obsolete validate-identifier problem keys.
UI tweak
apps/condo/domains/common/components/Form/FormItem.ts
Changed label CSS from white-space: nowrap to white-space: wrap.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Client (SignIn/Register)
    participant Frontend2FA as SecondFactor UI
    participant Backend as Backend API
    participant AuthSvc as Auth Validation/DB

    User->>Client: Submit primary credentials
    Client->>Backend: authenticate* mutation (primary)
    Backend->>AuthSvc: validateUserCredentials(primary)
    alt Sufficient factors
        AuthSvc-->>Backend: success (session token)
        Backend-->>Client: auth token / success
        Client-->>User: redirect
    else NOT_ENOUGH_AUTH_FACTORS
        AuthSvc-->>Backend: error NOT_ENOUGH_AUTH_FACTORS + authDetails
        Backend-->>Client: NOT_ENOUGH_AUTH_FACTORS + availableSecondFactors, maskedData
        Client->>Frontend2FA: show second-factor UI with maskedData
        User->>Frontend2FA: select factor
        Frontend2FA->>Backend: startConfirmEmailAction/startConfirmPhoneAction
        Backend-->>Frontend2FA: code sent
        User->>Frontend2FA: enter code
        Frontend2FA->>Backend: authenticate* mutation + secondFactor
        Backend->>AuthSvc: validate secondFactor
        AuthSvc-->>Backend: success (session token)
        Backend-->>Client: auth token / success
        Client-->>User: redirect
    end
Loading
sequenceDiagram
    actor User
    participant Settings as User Settings UI
    participant SudoModal as SudoTokenProvider
    participant Backend as Backend API
    participant ConfirmSvc as ConfirmActionService

    User->>Settings: Click "Enable/Disable 2FA"
    Settings->>SudoModal: getSudoTokenWithModal()
    SudoModal->>User: show password prompt
    User->>SudoModal: submit password
    SudoModal->>Backend: generateSudoToken(password)
    alt NOT_ENOUGH_AUTH_FACTORS
        Backend-->>SudoModal: NOT_ENOUGH_AUTH_FACTORS + authDetails
        SudoModal->>Settings: open SecondFactor modal with authDetails
        User->>SudoModal: choose factor / request code
        SudoModal->>ConfirmSvc: startConfirm*Action(user/identifier)
        ConfirmSvc-->>SudoModal: code sent
        User->>SudoModal: enter code
        SudoModal->>Backend: generateSudoToken(confirmToken)
        Backend-->>SudoModal: sudo token
    else Direct success
        Backend-->>SudoModal: sudo token
    end
    SudoModal-->>Settings: return sudo token
    Settings->>Backend: changeTwoFactorAuthentication(sudoToken)
    Backend-->>Settings: success
    Settings-->>User: updated 2FA state
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • tolmachev21
  • nomerdvadcatpyat
  • toplenboren

Poem

🐰 I nibbled bytes and hid a bit,

I masked an email, left a slit.
A code hopped in on sleepy wings,
Two hops now guard the burrow's things.
Hooray — secure are all our springs.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: adding two-factor authentication (2FA) to the frontend of the Condo application for DOMA-12564.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/condo/DOMA-12564/2fa-frontend

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.40.5)
apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.test.js

[]

apps/condo/domains/user/utils/serverSchema/auth.spec.js

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
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: 11

Caution

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

⚠️ Outside diff range comments (1)
apps/condo/domains/user/schema/ConfirmEmailActionService.js (1)

321-335: Rate limiting uses non-normalized email while storage uses normalized email.

Line 335 calls checkEmailSendingLimits(context, email, ip) with the raw email value, but the ConfirmEmailAction is created with normalizedEmail (line 352). This inconsistency could allow rate limit bypass by using different email casing/formatting.

Additionally, when userFromInput?.id is provided but the user has no email set, email will be null, which will fail at normalizeEmail(email) with a potentially confusing error message.

Consider normalizing email before rate limiting and adding explicit handling for missing user email:

                if (userFromInput?.id) {
                    const user = await User.getOne(context, { id: userFromInput.id, deletedAt: null }, 'id email')
                    email = user?.email || null
+                   if (!email) {
+                       throw new GQLError({
+                           mutation: 'startConfirmEmailAction',
+                           code: BAD_USER_INPUT,
+                           type: 'USER_HAS_NO_EMAIL',
+                           message: 'User does not have an email address',
+                       }, context)
+                   }
                } else {
                    email = emailFromInput
                }

                const normalizedEmail = normalizeEmail(email)
                if (!normalizedEmail) {
                    throw new GQLError(ERRORS.WRONG_EMAIL_FORMAT, context)
                }

-               await checkEmailSendingLimits(context, email, ip)
+               await checkEmailSendingLimits(context, normalizedEmail, ip)
🧹 Nitpick comments (37)
apps/condo/domains/common/utils/helpers.ts (1)

21-35: Masked phone formatting logic is sound; ensure input shape matches masked regex expectations

The new masked regexes and isMaskedPhone flag preserve existing behavior for normal phones and correctly avoid normalizePhone so * characters aren’t stripped. One thing to watch: RUSSIAN_MASKED_PHONE_FORMAT_REGEXP / SPANISH_MASKED_PHONE_FORMAT_REGEXP assume the portion after +7/+34 is a contiguous block of digits/* without spaces or other separators. If maskPhone (or any caller) ever passes already formatted masked values like +7 *** *** ** **, these patterns won’t match and you’ll quietly fall back to the unformatted string.

If that’s a possible input, consider:

  • Either normalizing masked input to +7/+34 + [0-9*]+ before calling formatPhone(..., true), or
  • Relaxing the masked regexes to tolerate spaces/parentheses, and documenting the expected format via a short JSDoc on formatPhone.

Overall, behavior for existing callers (isMaskedPhone defaulting to false) remains unchanged.

apps/condo/domains/user/constants/authFactors.js (1)

1-10: AUTH_FACTOR_TYPES enum is clear and matches intended 2FA factor types

The constant names and string values (confirmPhoneToken, confirmEmailToken, password) are self-explanatory and will keep multi-factor flows readable. If you want to guard against accidental mutation, you could optionally freeze it:

const AUTH_FACTOR_TYPES = Object.freeze({
    CONFIRM_PHONE_TOKEN: 'confirmPhoneToken',
    CONFIRM_EMAIL_TOKEN: 'confirmEmailToken',
    PASSWORD: 'password',
})

but that’s not strictly necessary.

apps/condo/lang/ru/ru.json (1)

2391-2392: Profile 2FA section copy is understandable; factor placeholder keeps it flexible

pages.condo.profile.2FA.title / .tooltip correctly describe the feature and guide the user to the “Информация” tab. Using {factors} as a placeholder allows you to localize or vary the required factor list at the call site (e.g. “почту и телефон”) without touching translations again, which is a good choice.

Optionally, ensure the {factors} phrase passed from code is also localized (rather than hard‑coding Russian in JSX), to keep copy consistent across languages.

apps/condo/domains/common/utils/phone.spec.js (1)

78-95: LGTM! Comprehensive test coverage for the masking function.

The parameterized tests cover edge cases well, including boundary conditions for different string lengths. The masking logic is thoroughly verified.

Minor nit: The test description "should correct work" could be "should work correctly" for grammatical accuracy.

-    test.each(cases)('should correct work (%p)', (input, output) => {
+    test.each(cases)('should work correctly (%p)', (input, output) => {
apps/condo/domains/common/utils/mail.spec.js (1)

1-1: maskEmail test coverage matches implementation and common cases

The new import and maskEmail() tests look correct and align with the helper’s behavior (first 1–2 chars + *** and full domain). You’re covering several username lengths and a non‑trivial domain (yandex.ru), which should prevent regressions.

If you want to harden further, you could optionally add a case for edge inputs (e.g. '@example.com' or already-lower/uppercased emails), but it’s not strictly necessary.

Also applies to: 68-82

apps/condo/domains/common/utils/phone.js (1)

13-40: maskPhone logic is sound; consider a small defensive guard

The masking rules (preserve optional +, reveal 1–2 leading and 1–2 trailing digits, mask the rest) are consistent and match typical phone masking expectations. Export wiring is correct.

To reduce the chance of runtime errors if a non-string sneaks in (e.g. undefined before normalization), you could add a lightweight type guard:

-function maskPhone (normalizedPhone) {
-    const hasPlus = normalizedPhone.startsWith('+')
-    const digits = normalizedPhone.replace(/\D/g, '')
+function maskPhone (normalizedPhone) {
+    if (typeof normalizedPhone !== 'string') return normalizedPhone
+
+    const hasPlus = normalizedPhone.startsWith('+')
+    const digits = normalizedPhone.replace(/\D/g, '')

This keeps current behavior for valid inputs while failing gracefully for unexpected ones.

Also applies to: 45-45

apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.test.js (1)

14-15: 2FA tests are well‑designed; consider loosening factor list assertion

The new imports and 2FA tests correctly exercise:

  • Failure path when 2FA is enabled but only password is provided (expecting NOT_ENOUGH_AUTH_FACTORS with authDetails and masked email/phone).
  • Success paths for both confirmPhoneToken and confirmEmailToken, including verifying that related Confirm*Action records are marked as completed.

That gives good coverage of the new behavior.

To make the tests slightly less brittle against future additions or ordering changes of second factors, you could switch:

availableSecondFactors: ['confirmEmailToken', 'confirmPhoneToken'],

to something like:

availableSecondFactors: expect.arrayContaining(['confirmEmailToken', 'confirmPhoneToken']),

but this is an optional hardening, not a blocker.

Also applies to: 31-35, 183-263

apps/condo/domains/common/utils/mail.js (1)

57-70: maskEmail implementation aligns with tests and intended masking

The helper correctly:

  • Leaves invalid shapes ('no-at-sign') untouched by returning the original value when there is no domain.
  • For valid emails, reveals up to the first two user characters and appends ***@domain, matching the new test cases.

If you want parity with maskPhone and a bit more robustness, you could optionally guard against non-string inputs in the same way, but current behavior is acceptable given the “normalizedEmail” contract.

Also applies to: 73-76

apps/condo/domains/user/schema/GenerateSudoTokenService.test.js (1)

16-17: GenerateSudoToken 2FA tests correctly exercise new error paths

The added imports and tests do a good job of:

  • Ensuring missing identity (no phone/email/confirm tokens, only password) yields CREDENTIAL_VALIDATION_FAILED.
  • Verifying that a 2FA‑enabled user with only a password gets NOT_ENOUGH_AUTH_FACTORS plus authDetails that include is2FAEnabled, userId, availableSecondFactors, and masked email/phone using the shared helpers.

That keeps the sudo‑token flow consistent with the other auth services.

As with the email/password tests, you might optionally relax the availableSecondFactors assertion to:

availableSecondFactors: expect.arrayContaining(['confirmEmailToken', 'confirmPhoneToken']),

to make future factor additions less disruptive, but that’s a nice‑to‑have.

Also applies to: 125-139, 178-204

apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.test.js (1)

1276-1305: NOT_ENOUGH_AUTH_FACTORS test is well-structured and verifies masking payload

  • The test correctly models a user with 2FA enabled and only one factor (phone token) provided.
  • Expected error:
    • type: 'NOT_ENOUGH_AUTH_FACTORS'
    • authDetails containing is2FAEnabled, userId, availableSecondFactors, and maskedData.email.
  • Using maskEmail(userAttrs.email) in the expectation keeps the test resilient to masking implementation details while still asserting that sensitive data is properly obfuscated in the error payload.

This is a solid, behavior-focused test for the new 2FA error path.

If you want to make this slightly more future‑proof, you could also assert that availableSecondFactors contains the expected values at least (e.g. via expect.arrayContaining) instead of an exact array, so adding new factors won’t break the test.

apps/condo/lang/es/es.json (3)

718-731: Unify tone and 2FA terminology; align resend phrasing.

Use tuteo (“Introduce …”), “autenticación” instead of “autorización”, “correo electrónico” over “email”, and “Reenviar código” for brevity and consistency.

-  "component.SecondFactorForm.title.forAuthorization": "Segundo factor de autorización",
+  "component.SecondFactorForm.title.forAuthorization": "Segundo factor de autenticación",
-  "component.SecondFactorForm.type.confirmEmailToken.description": "Introduce el código enviado a {email}",
+  "component.SecondFactorForm.type.confirmEmailToken.description": "Introduce el código enviado a {email}",
-  "component.SecondFactorForm.type.confirmEmailToken.label": "Por email",
+  "component.SecondFactorForm.type.confirmEmailToken.label": "Por correo electrónico",
-  "component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Enviar el código de nuevo",
+  "component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Reenviar código",
-  "component.SecondFactorForm.type.confirmPhoneToken.description": "Ingrese el código enviado a {phone}",
+  "component.SecondFactorForm.type.confirmPhoneToken.description": "Introduce el código enviado al número {phone}",
-  "component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Enviar el código de nuevo",
+  "component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Reenviar código",
-  "component.SecondFactorForm.type.password.description": "Introduzca la contraseña",
+  "component.SecondFactorForm.type.password.description": "Introduce la contraseña"

734-738: Consistent tuteo and action verbs for enable/disable.

Switch to “Introduce …” and use the “activar/desactivar” pair.

-  "component.SudoTokenProvider.modal.password.label": "Introduzca la contraseña",
+  "component.SudoTokenProvider.modal.password.label": "Introduce la contraseña",
-  "component.SudoTokenProvider.modal.password.title.disable2FA": "Para desactivar la autenticación de dos factores, necesitas ingresar una contraseña.",
+  "component.SudoTokenProvider.modal.password.title.disable2FA": "Para desactivar la autenticación de dos factores, introduce la contraseña.",
-  "component.SudoTokenProvider.modal.password.title.enable2FA": "Para habilitar la autenticación de dos factores, necesitas ingresar una contraseña.",
+  "component.SudoTokenProvider.modal.password.title.enable2FA": "Para activar la autenticación de dos factores, introduce la contraseña."

2391-2393: Match register and phrasing in tooltip.

Prefer tuteo and simpler phrasing; use Spanish quotes consistent with the file.

-  "pages.condo.profile.2FA.tooltip": "Para permitirnos enviarle el código, por favor proporcione sus {factors}. Para hacerlo, haga clic en \"Editar\" en la pestaña \"Información\" y agregue cualquier información que falte.",
+  "pages.condo.profile.2FA.tooltip": "Para poder enviarte el código, añade tus {factors}. Para ello, haz clic en «Editar» en la pestaña «Información» y añade la información que falte."
apps/condo/domains/user/components/UserSettingsContent.tsx (1)

249-252: Consider adding error handling for better UX.

If change2FA throws an error (which it does on failure), there's no error handling here to provide user feedback. The switch UI may appear unresponsive without any indication of what went wrong.

     const handle2FAEnabledChange: SwitchChangeEventHandler = useCallback(async (checked) => {
-        await change2FA(checked)
-        setIs2FAEnabled(checked)
+        try {
+            await change2FA(checked)
+            setIs2FAEnabled(checked)
+        } catch (e) {
+            // Error already logged in change2FA
+            // Optionally show notification to user here
+        }
     }, [change2FA])
apps/condo/lang/en/en.json (1)

718-730: Second factor copy is clear; only minor wording polish possible

The new 2FA strings read well and are consistent with surrounding auth copy. If you want slightly more natural English, consider tweaks like:

  • "Second factor of authorization""Second authentication factor" or "Two-factor authentication".
  • "By password""Using password".

Purely stylistic; current wording is acceptable.

Also applies to: 734-737

apps/condo/domains/user/utils/serverSchema/auth.js (2)

192-222: Token-derived phone/email fallback is reasonable; consider tiny optimization

Letting _getUser derive phone/email from confirmPhoneToken / confirmEmailToken whenever both identifiers are undefined makes the flow more flexible for token‑only auth/2FA scenarios and still relies on _matchUser* to enforce expiry and verification.

If you ever see performance pressure on User queries, you could early‑return when both tokens resolve to no actions (i.e. phone and email remain undefined) instead of querying itemsQuery('User', …) with only { type, deletedAt: null }. This is purely a micro‑optimization; behaviour today is correct.


139-139: Return-type JSDoc is accurate but hard to maintain

The updated @return JSDoc for validateUserCredentials and _matchUser reflects the richer _error payload, but the union type is now quite long and duplicated.

Consider introducing a named typedef (e.g. @typedef AuthErrorPayload) and reusing it in both places to reduce drift and keep the docs readable.

Also applies to: 264-264

apps/condo/pages/auth/change-password.tsx (2)

546-554: Use JSX children instead of children prop.

As flagged by Biome, passing children via prop is not the canonical React pattern.

Apply this diff:

                            <Button
                                key='submit'
                                type='primary'
                                loading={isLoading}
                                htmlType='submit'
-                               children={`${SaveMsg} ${AndSignInMessage}`}
                                block
-                           />
+                           >
+                               {`${SaveMsg} ${AndSignInMessage}`}
+                           </Button>

525-525: Consider using a ternary for clarity.

The conditional style expression style={(currentSecondFactor || step !== 'changePassword') && { display: 'none' }} could evaluate to false, which while handled by React, is less explicit than a ternary.

-                <Row justify='center' style={(currentSecondFactor || step !== 'changePassword') && { display: 'none' }}>
+                <Row justify='center' style={(currentSecondFactor || step !== 'changePassword') ? { display: 'none' } : undefined}>
apps/condo/gql/index.ts (1)

7307-7339: 2FA mutation hook is consistent with generated types and surrounding auth mutations

The changeTwoFactorAuthentication operation, TS mutation function alias, hook, and exported helper types all line up with the definitions in operation.types.ts and follow the existing codegen pattern used for other auth mutations in this file. No issues from the frontend/graphql side.

If you find yourself manually refetching authenticatedUser after toggling 2FA, consider (in the source .graphql operation, not this generated file) returning the updated user booleans (e.g. isTwoFactorAuthenticationEnabled) from this mutation to simplify optimistic updates and cache syncing. Based on learnings, this file is generated, so changes should come from the schema/operations, not here.

apps/condo/domains/user/schema/ConfirmEmailActionService.js (1)

86-101: TODO comments indicate missing translations.

The error definitions include TODO(DOMA-12564): translate comments for messageForUser fields. Ensure these translations are added before release to provide proper localized error messages to users.

Would you like me to open an issue to track the translation work for these error messages?

apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.js (1)

135-151: Consider extracting 2FA error handling to a shared utility.

This pattern of checking for NOT_ENOUGH_AUTH_FACTORS and throwing a detailed error with authDetails is duplicated across multiple authentication services (this file and AuthenticateUserWithPhoneAndPasswordService.js based on the summary). Consider extracting this to a shared helper function.

apps/condo/domains/user/components/auth/SignInForm.tsx (2)

228-247: SecondFactorHeader creates new component instance on each render.

Using useCallback for a component that returns JSX doesn't prevent re-renders of its children. The callback itself is stable, but when called, it returns fresh JSX. Consider extracting this to a separate component or using useMemo for the JSX if you want to prevent re-renders.


108-118: Consider using TypeScript interface for handleSubmit parameters.

The handleSubmit function accepts many parameters as an object. Defining a TypeScript interface would improve type safety and documentation.

interface SecondFactorSubmitParams {
    isLoading: boolean
    setIsLoading: (loading: boolean) => void
    userIdRef: React.MutableRefObject<string | null>
    setUserMaskedData: (data: { email?: string; phone?: string } | null) => void
    setAvailableSecondFactors: (factors: string[]) => void
    setCurrentSecondFactor: (factor: string | null) => void
    currentSecondFactor: string | null
    confirmEmailToken: string | null
    confirmPhoneToken: string | null
}
apps/condo/domains/user/components/auth/RegisterForm.tsx (1)

211-231: Duplicate SecondFactorHeader component between SignInForm and RegisterForm.

The SecondFactorHeader implementation is nearly identical in both files. Consider extracting this to a shared component to reduce duplication.

// In a shared file like SecondFactorHeader.tsx
export const createSecondFactorHeader = (title: string, onBack: () => void) => (
    <Col span={24}>
        <Row gutter={[0, 16]}>
            <Col span={24}>
                <Space direction='vertical' size={24}>
                    <Button.Icon onClick={onBack} size='small'>
                        <ArrowLeft />
                    </Button.Icon>
                    <Typography.Title level={2}>{title}</Typography.Title>
                </Space>
            </Col>
        </Row>
    </Col>
)
apps/condo/domains/user/components/SudoTokenProvider.tsx (1)

567-567: Using string replace instead of intl.formatMessage for placeholders.

Lines 567 and 591 use .replace('{phone}', ...) and .replace('{email}', ...) for placeholder substitution. This bypasses the standard i18n pattern and could cause issues if the translation format changes.

Consider using intl.formatMessage with values:

const ConfirmPhoneTokenDescription = intl.formatMessage(
    { id: 'component.SecondFactorForm.type.confirmPhoneToken.description' },
    { phone: formatPhone(userMaskedData?.phone || '', true) }
)

Then use the message directly without .replace().

Also applies to: 591-591

apps/condo/domains/user/components/auth/SecondFactorForm.tsx (4)

119-122: Redundant expiry date setting.

The expiry date is set twice: once before the mutation (line 122) and again after (line 140) with identical values. The first assignment provides immediate rate-limiting UI feedback, but if that's the intent, the second assignment is unnecessary.

         const shouldWait = confirmEmailTokenExpiredDate && dayjs(confirmEmailTokenExpiredDate) > dayjs()
         if (shouldWait) return

         setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())

         const sender = getClientSideSenderInfo()
         const captcha = await executeCaptcha()

         const res = await startConfirmEmailActionMutation({
             variables: {
                 data: {
                     dv: 1,
                     sender,
                     captcha,
                     user: {
                         id: userIdRef.current,
                     },
                 },
             },
         })

-        setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
-
         const token = res?.data?.result?.token

Also applies to: 140-140


254-265: Missing dependencies in useEffect.

The effect depends on startConfirmPhoneAction and startConfirmEmailAction callbacks but they're omitted from the dependency array. This could lead to stale closure issues if the callbacks reference outdated state. If this is intentional to only trigger on currentSecondFactor changes, consider adding an ESLint disable comment with an explanation.

     useEffect(() => {
         if (currentSecondFactor !== 'confirmEmailToken' && currentSecondFactor !== 'confirmPhoneToken') return
         if (!userIdRef.current) return

         if (currentSecondFactor === 'confirmPhoneToken') {
             // send sms-code by userId
             startConfirmPhoneAction()
         } else if (currentSecondFactor === 'confirmEmailToken') {
             // send email-code by userId
             startConfirmEmailAction()
         }
+        // eslint-disable-next-line react-hooks/exhaustive-deps -- Only trigger when currentSecondFactor changes
     }, [currentSecondFactor])

230-244: Fragile error message extraction with @ts-ignore.

The error extraction uses @ts-ignore and accesses a deeply nested path that may not match Apollo's error structure. Consider using a type-safe utility or properly typing the response to avoid runtime issues.


31-37: Consider adding TypeScript types for hook parameters and return value.

The useSecondFactor hook lacks TypeScript type definitions for its parameters and return value. Adding proper types would improve IDE support and catch potential misuse at compile time.

type UseSecondFactorParams = {
    form: FormInstance
    onSubmit: (params: OnSubmitParams) => Promise<void>
    Header?: React.ComponentType<HeaderProps>
    isLoading: boolean
    setIsLoading: React.Dispatch<React.SetStateAction<boolean>>
}

type UseSecondFactorReturn = {
    SecondFactorForm: React.ReactNode
    ProblemsModal: React.ReactNode
    onSubmitWithSecondFactor: () => Promise<void>
    currentSecondFactor: 'confirmPhoneToken' | 'confirmEmailToken' | 'password' | null
}
apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js (2)

55-62: TODO comment indicates missing translations.

The TODO on line 60 indicates translations need to be added for api.user.NOT_ENOUGH_AUTH_FACTORS. Ensure this is tracked and completed before release to avoid showing untranslated error messages to users.

Would you like me to open an issue to track this translation task?


150-162: Consider error handling for ConfirmAction updates.

If the ConfirmEmailAction.update or ConfirmPhoneAction.update calls fail after successful validation, the user authentication succeeds but the action completion isn't recorded. This could leave orphaned action records. Consider wrapping in try-catch or handling failures gracefully.

apps/condo/schema.ts (3)

2457-2478: Email+password second factor type/enum mirror token flow; consider enum reuse only if desired

The AuthenticateUserWithEmailAndPasswordSecondFactorInput and its enum (ConfirmEmailToken, ConfirmPhoneToken) are consistent with the token flow, and attaching them via secondFactor on the input looks correct. If you find yourself duplicating these exact enum variants across multiple flows, you might consider a shared GraphQL enum for “email/phone token” factors to avoid drift over time, but it’s not strictly necessary.


96333-96340: StartConfirmEmailActionInput now allows user reference; ensure invariants are validated

Making email optional and adding user?: UserWhereUniqueInput lets callers initiate email confirmation either by raw email or by user id, which is flexible. However, the type alone now permits requests with neither field (or conflicting values), so please make sure the resolver enforces a clear contract (e.g. “require at least one of email or user, and if both are provided they must match”) and that any corresponding API docs describe this behavior.


96347-96353: StartConfirmPhoneActionInput mirrors email flow; same validation concern

phone becoming optional and user?: UserWhereUniqueInput being added gives analogous flexibility for phone confirmations. As with email, the schema now permits calls without either phone or user, so the resolver should mandate at least one identifier and define how to handle both present (e.g. mismatch cases). Worth double‑checking tests for these edge conditions.

apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.js (2)

161-176: TODO comments for translations should be tracked.

The error definitions follow existing patterns, but the TODO comments for translations at lines 166-167 and 174-175 should be addressed before the feature is fully released to ensure proper user-facing messages.

Also, minor inconsistency: NOT_ENOUGH_AUTH_FACTORS uses an imported constant for type (line 164), while UNEXPECTED_SECOND_FACTOR uses a string literal (line 172). Consider using an imported constant for consistency:

     UNEXPECTED_SECOND_FACTOR: {
         mutation: 'authenticateOrRegisterUserWithToken',
         code: BAD_USER_INPUT,
-        type: 'UNEXPECTED_SECOND_FACTOR',
+        type: UNEXPECTED_SECOND_FACTOR, // import from constants/errors
         message: 'Unexpected second factor',

Do you want me to open an issue to track the missing translations for DOMA-12564?


430-448: Second factor validation logic is sound.

The logic correctly prevents using the same factor type for both primary and secondary authentication. The validation against supportedSecondFactors properly enforces the constraint.

One observation: lines 440, 443, and 446 use secondFactor?.value || '' as a fallback. Since the GraphQL schema defines value: String! (required), this fallback should never be triggered. However, if it were, passing an empty string would likely cause downstream validation to fail silently rather than explicitly. Consider logging or handling this edge case more explicitly if you want stricter guarantees:

-                            if (secondFactor.type === AUTH_FACTOR_TYPES.CONFIRM_EMAIL_TOKEN) {
-                                authFactors.confirmEmailToken = secondFactor?.value || ''
-                            }
+                            if (secondFactor.type === AUTH_FACTOR_TYPES.CONFIRM_EMAIL_TOKEN) {
+                                authFactors.confirmEmailToken = secondFactor.value
+                            }

Since the schema already enforces the requirement, the defensive || '' may be unnecessary.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54e4442 and 77bb08b.

📒 Files selected for processing (40)
  • apps/condo/domains/common/utils/helpers.ts (1 hunks)
  • apps/condo/domains/common/utils/mail.js (1 hunks)
  • apps/condo/domains/common/utils/mail.spec.js (2 hunks)
  • apps/condo/domains/common/utils/phone.js (1 hunks)
  • apps/condo/domains/common/utils/phone.spec.js (2 hunks)
  • apps/condo/domains/user/components/SudoTokenProvider.tsx (14 hunks)
  • apps/condo/domains/user/components/UserSettingsContent.tsx (9 hunks)
  • apps/condo/domains/user/components/auth/RegisterForm.tsx (11 hunks)
  • apps/condo/domains/user/components/auth/SecondFactorForm.tsx (1 hunks)
  • apps/condo/domains/user/components/auth/SignInForm.tsx (5 hunks)
  • apps/condo/domains/user/components/auth/ValidateIdentifierForm.tsx (7 hunks)
  • apps/condo/domains/user/constants/authFactors.js (1 hunks)
  • apps/condo/domains/user/constants/errors.js (2 hunks)
  • apps/condo/domains/user/gql.js (2 hunks)
  • apps/condo/domains/user/queries/ChangeTwoFactorAuthentication.graphql (1 hunks)
  • apps/condo/domains/user/queries/User.graphql (1 hunks)
  • apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.js (5 hunks)
  • apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.test.js (4 hunks)
  • apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.js (5 hunks)
  • apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.test.js (3 hunks)
  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js (5 hunks)
  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.test.js (3 hunks)
  • apps/condo/domains/user/schema/ConfirmEmailActionService.js (6 hunks)
  • apps/condo/domains/user/schema/ConfirmEmailActionService.test.js (4 hunks)
  • apps/condo/domains/user/schema/ConfirmPhoneActionService.js (5 hunks)
  • apps/condo/domains/user/schema/ConfirmPhoneActionService.test.js (1 hunks)
  • apps/condo/domains/user/schema/GenerateSudoTokenService.js (3 hunks)
  • apps/condo/domains/user/schema/GenerateSudoTokenService.test.js (3 hunks)
  • apps/condo/domains/user/utils/serverSchema/auth.js (8 hunks)
  • apps/condo/domains/user/utils/serverSchema/auth.spec.js (30 hunks)
  • apps/condo/domains/user/utils/testSchema/index.js (3 hunks)
  • apps/condo/gql/index.ts (2 hunks)
  • apps/condo/gql/operation.types.ts (2 hunks)
  • apps/condo/lang/en/en.json (2 hunks)
  • apps/condo/lang/es/es.json (2 hunks)
  • apps/condo/lang/ru/ru.json (2 hunks)
  • apps/condo/pages/auth/change-password.tsx (13 hunks)
  • apps/condo/pages/auth/signin.tsx (2 hunks)
  • apps/condo/schema.graphql (9 hunks)
  • apps/condo/schema.ts (11 hunks)
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-09-17T13:39:10.781Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6645
File: apps/dev-portal-web/domains/common/components/auth/LoginForm.tsx:1-3
Timestamp: 2025-09-17T13:39:10.781Z
Learning: In the condo project, authMethods fallback is handled at the Next.js configuration level in next.config.ts with AUTH_METHODS defaulting to ["condo"], making direct destructuring of publicRuntimeConfig.authMethods safe without additional guards in components.

Applied to files:

  • apps/condo/pages/auth/signin.tsx
  • apps/condo/domains/user/components/SudoTokenProvider.tsx
  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js
  • apps/condo/domains/user/components/auth/SignInForm.tsx
  • apps/condo/domains/user/components/auth/SecondFactorForm.tsx
  • apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.js
  • apps/condo/domains/user/utils/serverSchema/auth.js
  • apps/condo/domains/user/components/UserSettingsContent.tsx
  • apps/condo/domains/user/components/auth/RegisterForm.tsx
  • apps/condo/pages/auth/change-password.tsx
📚 Learning: 2025-09-17T13:39:10.781Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6645
File: apps/dev-portal-web/domains/common/components/auth/LoginForm.tsx:1-3
Timestamp: 2025-09-17T13:39:10.781Z
Learning: In the condo project, authMethods fallback is handled at the Next.js configuration level in next.config.ts with AUTH_METHODS defaulting to '["condo"]' via JSON.parse(conf['AUTH_METHODS'] || '["condo"]'), making direct destructuring of publicRuntimeConfig.authMethods safe without additional guards in components.

Applied to files:

  • apps/condo/pages/auth/signin.tsx
  • apps/condo/domains/user/constants/authFactors.js
  • apps/condo/domains/user/components/SudoTokenProvider.tsx
  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js
  • apps/condo/domains/user/components/auth/SignInForm.tsx
  • apps/condo/domains/user/schema/GenerateSudoTokenService.js
  • apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.js
  • apps/condo/domains/user/utils/serverSchema/auth.js
  • apps/condo/domains/user/components/UserSettingsContent.tsx
  • apps/condo/domains/user/components/auth/RegisterForm.tsx
  • apps/condo/pages/auth/change-password.tsx
📚 Learning: 2025-09-17T13:38:39.633Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6645
File: apps/dev-portal-web/domains/common/components/auth/AuthForm.tsx:12-12
Timestamp: 2025-09-17T13:38:39.633Z
Learning: In the condo project, runtime configuration fallbacks for authMethods are handled at the Next.js config level in next.config.ts, where AUTH_METHODS defaults to ['condo'] if not provided via environment variables, ensuring safe destructuring in components that access publicRuntimeConfig.authMethods.

Applied to files:

  • apps/condo/pages/auth/signin.tsx
  • apps/condo/domains/user/components/SudoTokenProvider.tsx
  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js
  • apps/condo/domains/user/components/auth/SignInForm.tsx
  • apps/condo/domains/user/utils/serverSchema/auth.js
  • apps/condo/domains/user/components/UserSettingsContent.tsx
  • apps/condo/domains/user/components/auth/RegisterForm.tsx
  • apps/condo/pages/auth/change-password.tsx
📚 Learning: 2025-09-17T13:38:39.633Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6645
File: apps/dev-portal-web/domains/common/components/auth/AuthForm.tsx:12-12
Timestamp: 2025-09-17T13:38:39.633Z
Learning: In the condo project, runtime configuration fallbacks for authMethods are handled at the Next.js config level in next.config.ts, where AUTH_METHODS defaults to ["condo"] via JSON.parse(conf['AUTH_METHODS'] || '["condo"]') if not provided via environment variables, ensuring safe destructuring in components that access publicRuntimeConfig.authMethods.

Applied to files:

  • apps/condo/pages/auth/signin.tsx
  • apps/condo/domains/user/components/SudoTokenProvider.tsx
  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js
  • apps/condo/domains/user/components/auth/SignInForm.tsx
  • apps/condo/domains/user/utils/serverSchema/auth.js
  • apps/condo/domains/user/components/UserSettingsContent.tsx
  • apps/condo/pages/auth/change-password.tsx
📚 Learning: 2025-08-18T07:33:48.748Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6533
File: apps/condo/domains/user/integration/passport/errors.js:72-76
Timestamp: 2025-08-18T07:33:48.748Z
Learning: The UNKNOWN_CONFIRM_TOKEN_TYPE error in apps/condo/domains/user/integration/passport/errors.js uses INTERNAL_ERROR classification because it can only be caused by wrong server utility use (e.g., passing invalid token types to internal functions), not by user input.

Applied to files:

  • apps/condo/domains/user/constants/errors.js
  • apps/condo/domains/user/schema/ConfirmPhoneActionService.test.js
  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js
  • apps/condo/domains/user/schema/GenerateSudoTokenService.js
  • apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.test.js
  • apps/condo/domains/user/schema/ConfirmEmailActionService.js
  • apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.js
  • apps/condo/domains/user/utils/serverSchema/auth.js
  • apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.js
📚 Learning: 2025-09-30T13:04:44.424Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6701
File: apps/dev-portal-api/domains/miniapp/schema/GetB2CAppInfoService.js:36-48
Timestamp: 2025-09-30T13:04:44.424Z
Learning: In the open-condo-software/condo repository using Keystone framework, thrown errors in GraphQL resolvers are handled globally by Keystone, so explicit try-catch blocks for error handling in resolver functions are not required.

Applied to files:

  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.test.js
  • apps/condo/domains/user/utils/serverSchema/auth.js
📚 Learning: 2025-08-18T07:35:07.758Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6533
File: apps/condo/domains/user/integration/passport/utils/routes.js:3-11
Timestamp: 2025-08-18T07:35:07.758Z
Learning: In apps/condo/domains/user/integration/passport/utils/routes.js, the checkAuthRateLimits middleware runs after other middleware that ensures req.session exists, so defensive programming with optional chaining for req.session?.userType is not needed.

Applied to files:

  • apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js
  • apps/condo/domains/user/schema/ConfirmEmailActionService.js
  • apps/condo/domains/user/utils/serverSchema/auth.js
📚 Learning: 2025-08-04T08:04:46.817Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6472
File: apps/condo/schema.graphql:390-394
Timestamp: 2025-08-04T08:04:46.817Z
Learning: In the condo codebase, the schema.graphql file is auto-generated (codegen) from Keystone 5 schema definitions. Security and access controls are implemented in the actual Keystone schema files (like User.js) using access control properties such as SERVER_CREATE_ONLY_ACCESS, not in the generated GraphQL schema file. When reviewing schema.graphql, consider that field presence doesn't indicate lack of access controls - check the corresponding Keystone schema files for actual security implementation.

Applied to files:

  • apps/condo/gql/index.ts
  • apps/condo/domains/user/utils/serverSchema/auth.js
📚 Learning: 2025-09-17T13:40:42.895Z
Learnt from: SavelevMatthew
Repo: open-condo-software/condo PR: 6645
File: apps/dev-portal-web/pages/_app.tsx:40-41
Timestamp: 2025-09-17T13:40:42.895Z
Learning: In the condo project, runtime configuration fallbacks for runtimeTranslations are handled at the Next.js config level in next.config.ts, where RUNTIME_TRANSLATIONS defaults to '{}' via JSON.parse(conf['RUNTIME_TRANSLATIONS'] || '{}') if not provided via environment variables, ensuring safe destructuring in components that access publicRuntimeConfig.runtimeTranslations.

Applied to files:

  • apps/condo/domains/user/components/UserSettingsContent.tsx
🧬 Code graph analysis (18)
apps/condo/domains/common/utils/phone.js (2)
apps/condo/domains/user/schema/ConfirmPhoneActionService.js (1)
  • normalizedPhone (275-275)
apps/condo/domains/user/schema/GenerateSudoTokenService.js (1)
  • normalizedPhone (117-117)
apps/condo/pages/auth/signin.tsx (1)
apps/condo/domains/user/hooks/useAuthMethods.ts (1)
  • useAuthMethods (32-85)
apps/condo/domains/common/utils/mail.js (2)
apps/condo/domains/user/schema/ConfirmEmailActionService.js (2)
  • normalizedEmail (330-330)
  • user (324-324)
apps/condo/domains/user/schema/GenerateSudoTokenService.js (1)
  • normalizedEmail (116-116)
apps/condo/domains/user/schema/ConfirmEmailActionService.test.js (3)
apps/condo/domains/user/utils/testSchema/index.js (16)
  • client (177-179)
  • client (187-187)
  • client (198-198)
  • client (206-206)
  • client (214-214)
  • client (222-222)
  • client (319-319)
  • client (333-333)
  • client (354-354)
  • client (368-368)
  • client (382-382)
  • client (397-397)
  • client (411-411)
  • client (427-427)
  • client (509-509)
  • client (553-553)
apps/dev-portal-api/domains/user/utils/testSchema/index.js (2)
  • startConfirmEmailActionByTestClient (217-217)
  • userAttrs (235-235)
apps/condo/domains/user/constants/confirmEmailAction.js (1)
  • CONFIRM_EMAIL_ACTION_MESSAGE_TYPES (7-10)
apps/condo/domains/user/schema/GenerateSudoTokenService.test.js (2)
packages/keystone/test.utils.js (1)
  • expectToThrowGQLErrorToResult (1026-1028)
apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js (1)
  • STAFF (94-104)
apps/condo/domains/user/components/SudoTokenProvider.tsx (4)
apps/condo/domains/user/components/auth/SecondaryLink.tsx (1)
  • SecondaryLink (8-14)
apps/condo/domains/user/constants/errors.js (1)
  • NOT_ENOUGH_AUTH_FACTORS (96-96)
apps/condo/gql/index.ts (4)
  • useStartConfirmEmailActionMutation (7723-7726)
  • useCompleteConfirmEmailActionMutation (7476-7479)
  • useStartConfirmPhoneActionMutation (7756-7759)
  • useCompleteConfirmPhoneActionMutation (7509-7512)
apps/condo/domains/common/utils/helpers.ts (1)
  • formatPhone (28-37)
apps/condo/domains/user/utils/testSchema/index.js (1)
apps/condo/domains/user/gql.js (1)
  • START_CONFIRM_PHONE_ACTION_MUTATION (224-228)
apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js (3)
apps/condo/domains/user/constants/errors.js (1)
  • NOT_ENOUGH_AUTH_FACTORS (96-96)
apps/condo/domains/user/constants/authFactors.js (1)
  • AUTH_FACTOR_TYPES (1-5)
apps/condo/domains/user/utils/serverSchema/index.js (2)
  • ConfirmEmailAction (148-148)
  • ConfirmPhoneAction (39-39)
apps/condo/domains/user/schema/GenerateSudoTokenService.js (3)
apps/condo/domains/user/constants/errors.js (2)
  • NOT_ENOUGH_AUTH_FACTORS (96-96)
  • ERRORS (237-306)
apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js (1)
  • ERRORS (22-63)
apps/condo/domains/user/utils/serverSchema/auth.js (1)
  • ERROR_TYPES (12-14)
apps/condo/domains/user/utils/serverSchema/auth.spec.js (1)
apps/condo/domains/user/utils/serverSchema/auth.js (7)
  • require (2-2)
  • require (4-4)
  • require (5-5)
  • require (6-6)
  • require (7-7)
  • require (8-8)
  • require (9-9)
apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.test.js (2)
apps/condo/domains/user/constants/common.js (2)
  • RESIDENT (2-2)
  • SERVICE (4-4)
apps/condo/domains/user/utils/tokens.js (1)
  • TOKEN_TYPES (4-8)
apps/condo/domains/user/components/auth/SecondFactorForm.tsx (5)
apps/condo/domains/user/components/auth/SecondaryLink.tsx (1)
  • SecondaryLink (8-14)
packages/miniapp-utils/src/helpers/urls.ts (1)
  • isSafeUrl (1-20)
apps/condo/gql/index.ts (4)
  • useStartConfirmEmailActionMutation (7723-7726)
  • useCompleteConfirmEmailActionMutation (7476-7479)
  • useStartConfirmPhoneActionMutation (7756-7759)
  • useCompleteConfirmPhoneActionMutation (7509-7512)
apps/condo/domains/user/components/containers/ResponsiveCol.tsx (1)
  • ResponsiveCol (10-29)
apps/condo/domains/common/utils/helpers.ts (1)
  • formatPhone (28-37)
apps/condo/gql/index.ts (1)
apps/condo/gql/operation.types.ts (2)
  • ChangeTwoFactorAuthenticationMutation (1134-1134)
  • ChangeTwoFactorAuthenticationMutationVariables (1129-1131)
apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.js (5)
apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js (2)
  • validation (123-130)
  • getSchemaCtx (164-164)
apps/condo/domains/user/constants/errors.js (1)
  • NOT_ENOUGH_AUTH_FACTORS (96-96)
apps/condo/domains/user/constants/authFactors.js (1)
  • AUTH_FACTOR_TYPES (1-5)
apps/condo/domains/user/utils/serverSchema/auth.js (6)
  • ERROR_TYPES (12-14)
  • getSchemaCtx (195-195)
  • getSchemaCtx (369-369)
  • getSchemaCtx (389-389)
  • getSchemaCtx (423-423)
  • getSchemaCtx (457-457)
apps/condo/domains/user/utils/serverSchema/index.js (2)
  • ConfirmPhoneAction (39-39)
  • ConfirmEmailAction (148-148)
apps/condo/domains/user/components/UserSettingsContent.tsx (6)
apps/condo/domains/user/schema/ConfirmEmailActionService.js (1)
  • user (324-324)
apps/condo/domains/user/schema/ConfirmPhoneActionService.js (1)
  • user (269-269)
apps/condo/domains/user/schema/ChangeTwoFactorAuthenticationService.js (2)
  • user (150-153)
  • sudoToken (136-141)
apps/condo/domains/user/components/SudoTokenProvider.tsx (1)
  • useSudoToken (51-51)
apps/condo/gql/index.ts (1)
  • useChangeTwoFactorAuthenticationMutation (7333-7336)
apps/condo/domains/user/schema/GenerateSudoTokenService.js (1)
  • sudoToken (162-168)
apps/condo/domains/user/components/auth/ValidateIdentifierForm.tsx (1)
apps/condo/domains/user/components/auth/SecondaryLink.tsx (1)
  • SecondaryLink (8-14)
apps/condo/domains/user/schema/ConfirmPhoneActionService.js (2)
packages/keystone/utils/errors/constants.js (1)
  • BAD_USER_INPUT (10-10)
apps/condo/domains/user/schema/ConfirmEmailActionService.js (5)
  • data (238-238)
  • data (428-428)
  • data (510-510)
  • ERRORS (57-141)
  • user (324-324)
apps/condo/pages/auth/change-password.tsx (4)
apps/condo/domains/user/constants/errors.js (1)
  • NOT_ENOUGH_AUTH_FACTORS (96-96)
apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.js (1)
  • authFactors (421-421)
apps/condo/domains/user/utils/tokens.js (1)
  • TOKEN_TYPES (4-8)
apps/condo/domains/user/components/auth/SecondFactorForm.tsx (1)
  • useSecondFactor (31-431)
🪛 Biome (2.1.2)
apps/condo/pages/auth/change-password.tsx

[error] 551-551: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🪛 Gitleaks (8.29.0)
apps/condo/domains/user/utils/serverSchema/auth.js

[high] 340-340: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ 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). (4)
  • GitHub Check: Build open-source part (ubuntu-22.04, 22.x, 3.13)
  • GitHub Check: Semgrep vulnerabilities check
  • GitHub Check: Check dependencies integrity (22.x)
  • GitHub Check: Lint source code (22.x)

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
13.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Member

@toplenboren toplenboren left a comment

Choose a reason for hiding this comment

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

mega job


function maskPhone (normalizedPhone) {
const hasPlus = normalizedPhone.startsWith('+')
const digits = normalizedPhone.replace(/\D/g, '')
Copy link
Member

Choose a reason for hiding this comment

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

^^

Copy link
Member

Choose a reason for hiding this comment

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

not replaceAll?

Comment on lines 80 to 91
['+79008007060', '+79*******60'],
['79008007060', '79*******60'],
['1', '1'],
['12', '1*'],
['123', '1*3'],
['1234', '1**4'],
['12345', '1***5'],
['123456', '1****6'],
['1234567', '12***67'],
['12345678', '12****78'],
['123456789', '12*****89'],
Copy link
Member

Choose a reason for hiding this comment

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

please also add negative cases, like '' or when not a phone nubmer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You must hand over the already normalized phone

Copy link
Member

Choose a reason for hiding this comment

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

same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed function to maskNormalizedPhone

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added case with ''

Comment on lines +69 to +78
const cases = [
['[email protected]', 'a***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'qw***@yandex.ru'],
]
Copy link
Member

Choose a reason for hiding this comment

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

best to add negative cases too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You must use only normalized emails

Copy link
Member

Choose a reason for hiding this comment

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

yes, but the function is named maskEmail, and not maskNormalizedEmail.. Really easy to forget to normalize and use your function. Also.. what about undefined case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed function to maskNormalizedEmail

@Alllex202 Alllex202 force-pushed the feat/condo/DOMA-12564/2fa-frontend branch from 77bb08b to c2a862b Compare January 20, 2026 14:29
@Alllex202 Alllex202 added ✋🙂 Review please Comments are resolved, take a look, please 🐘 BIG No so easy to review changes up to 1300 lines of code labels Jan 20, 2026
Copy link
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

🤖 Fix all issues with AI agents
In `@apps/condo/domains/user/components/auth/RegisterForm.tsx`:
- Around line 294-315: The NOT_ENOUGH_AUTH_FACTORS check is using the wrong path
(res?.errors?.graphQLErrors) so it never detects the error; update the checks to
inspect res?.errors directly (use res?.errors?.some(isNotEnoughAuthFactors) and
res?.errors?.find(isNotEnoughAuthFactors)) and then proceed to read
extensions/authDetails from that found error, keeping the existing logic that
sets userIdRef.current, setUserMaskedData, setAvailableSecondFactors,
setCurrentSecondFactor (using currentSecondFactor or availableSecondFactors[0])
and setStep('authenticate2FA'); also remove or adjust the `@ts-ignore` stubs and
ensure types align with GraphQLError[] if needed.

In `@apps/condo/domains/user/components/auth/SignInForm.tsx`:
- Around line 179-199: The NOT_ENOUGH_AUTH_FACTORS check is using the wrong
error path (res?.errors?.graphQLErrors) which is undefined for Apollo mutation
results; update the logic in the SignInForm mutation handling (the
isNotEnoughAuthFactors predicate usage around res) to inspect res?.errors
directly (e.g., res?.errors?.some(isNotEnoughAuthFactors) and
res?.errors.find(isNotEnoughAuthFactors)), then use the found
graphQLError.extensions.authDetails to populate userIdRef.current,
setUserMaskedData, setAvailableSecondFactors and setCurrentSecondFactor as
before; ensure null/undefined guards remain for authDetails,
availableSecondFactors and currentSecondFactor.

In `@apps/condo/domains/user/components/SudoTokenProvider.tsx`:
- Around line 265-285: The NOT_ENOUGH_AUTH_FACTORS check is using the wrong
error path (res?.errors?.graphQLErrors) which is undefined for Apollo mutation
results; update the logic in the block using isNotEnoughAuthFactors to inspect
res?.errors (an array of GraphQLFormattedError) instead of
res?.errors?.graphQLErrors, i.e. find the matching error via
res?.errors?.find(isNotEnoughAuthFactors) and then read
graphQLError.extensions.authDetails, then proceed to set userIdRef.current,
setUserMaskedData, setAvailableSecondFactors, setConfirmActionType,
setIsModalVisible and setIsSecondFactorModalVisible exactly as before.
♻️ Duplicate comments (7)
apps/condo/domains/user/components/SudoTokenProvider.tsx (3)

146-154: Consider adding error logging to empty onError handlers.

The completeConfirmEmailMutation and completeConfirmPhoneMutation have empty onError handlers, which silently ignore errors and make debugging difficult.

Proposed improvement
     const [completeConfirmEmailMutation] = useCompleteConfirmEmailActionMutation({
-        onError: () => {},
+        // NOTE: Errors are handled in handleChangeCodeInput response checking
+        onError: (error) => console.error('Complete confirm email action failed:', error),
     })
     // ...
     const [completeConfirmPhoneMutation] = useCompleteConfirmPhoneActionMutation({
-        onError: () => {},
+        // NOTE: Errors are handled in handleChangeCodeInput response checking
+        onError: (error) => console.error('Complete confirm phone action failed:', error),
     })

169-203: Duplicate expiry date setting and potential retry prevention on failure.

The confirmEmailTokenExpiredDate is set twice (lines 177 and 195). If the mutation fails, the expiry date is still set at line 177, preventing retries for a minute even on failure.

Proposed fix
     const startConfirmEmailAction = useCallback(async () => {
         if (!userIdRef.current) {
             throw new Error('No userid for StartConfirmEmailAction!')
         }

         const shouldWait = confirmEmailTokenExpiredDate && dayjs(confirmEmailTokenExpiredDate) > dayjs()
         if (shouldWait) return

-        setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())

         const sender = getClientSideSenderInfo()
         const captcha = await executeCaptcha()

         const res = await startConfirmEmailActionMutation({...})

-        setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())

         const token = res?.data?.result?.token
         if (token) {
             setConfirmEmailToken(token)
+            setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
         }

         return res
     }, [...])

Apply the same pattern to startConfirmPhoneAction (lines 205-239).


456-467: Missing dependencies in useEffect.

The effect calls startConfirmPhoneAction and startConfirmEmailAction but they're not in the dependency array, which could lead to stale closures.

Proposed fix
     useEffect(() => {
         if (currentSecondFactor !== 'confirmEmailToken' && currentSecondFactor !== 'confirmPhoneToken') return
         if (!userIdRef.current) return

         if (currentSecondFactor === 'confirmPhoneToken') {
             startConfirmPhoneAction()
         } else if (currentSecondFactor === 'confirmEmailToken') {
             startConfirmEmailAction()
         }
-    }, [currentSecondFactor])
+    }, [currentSecondFactor, startConfirmPhoneAction, startConfirmEmailAction])
apps/condo/domains/user/components/UserSettingsContent.tsx (1)

218-229: Incorrect error path for TOKEN_NOT_FOUND check.

The res.errors from Apollo's useMutation is a direct array of GraphQL errors, not an object with a graphQLErrors property. The current path res?.errors?.graphQLErrors will always be undefined, causing the sudo token retry logic to never trigger.

🐛 Proposed fix
             if (res.errors || res?.data?.result?.status !== 'ok') {
-                // `@ts-ignore`
-                const isExpiredSudoToken = res?.errors?.graphQLErrors?.some((gqlError) => gqlError?.extensions?.type === 'TOKEN_NOT_FOUND')
+                const isExpiredSudoToken = res?.errors?.some((gqlError) => gqlError?.extensions?.type === 'TOKEN_NOT_FOUND')
                 if (isExpiredSudoToken) {
apps/condo/domains/user/components/auth/SignInForm.tsx (1)

262-268: Missing router in useEffect dependency array.

The effect uses router via updateQuery but doesn't include it in the dependency array, which could cause stale closures.

Proposed fix
     useEffect(() => {
         if (currentSecondFactor) {
             updateQuery(router, { newParameters: { 'authStep': 'secondFactor' } }, { resetOldParameters: false, routerAction: 'replace' })
         } else {
             updateQuery(router, { newParameters: { 'authStep': 'firstFactor' } }, { resetOldParameters: false, routerAction: 'replace' })
         }
-    }, [currentSecondFactor])
+    }, [currentSecondFactor, router])
apps/condo/domains/user/components/auth/SecondFactorForm.tsx (2)

254-265: Missing dependencies in useEffect.

The effect calls startConfirmPhoneAction and startConfirmEmailAction but they're not in the dependency array, which could lead to stale closures.

Proposed fix
     useEffect(() => {
         if (currentSecondFactor !== 'confirmEmailToken' && currentSecondFactor !== 'confirmPhoneToken') return
         if (!userIdRef.current) return

         if (currentSecondFactor === 'confirmPhoneToken') {
             // send sms-code by userId
             startConfirmPhoneAction()
         } else if (currentSecondFactor === 'confirmEmailToken') {
             // send email-code by userId
             startConfirmEmailAction()
         }
-    }, [currentSecondFactor])
+    }, [currentSecondFactor, startConfirmPhoneAction, startConfirmEmailAction])

396-396: Missing dependencies in useMemo.

The SecondFactorForm memoization is missing userMaskedData (used on lines 312 and 335) and PasswordContinueMessage (used on line 385) from the dependency array, which could cause stale data to be displayed.

Proposed fix
-    }, [ConfirmEmailTokenDescription, ConfirmEmailTokenLabel, ConfirmPhoneTokenDescription, ConfirmPhoneTokenLabel, EmailProblemsMessage, Header, PasswordDescription, PasswordLabel, PhoneProblemsMessage, ResetPasswordMessage, SendEmailCodeAgainMessage, SendPhoneCodeAgainMessage, availableSecondFactors, currentSecondFactor, handleChangeSecondFactorInput, redirectUrl, startConfirmEmailAction, startConfirmPhoneAction])
+    }, [ConfirmEmailTokenDescription, ConfirmEmailTokenLabel, ConfirmPhoneTokenDescription, ConfirmPhoneTokenLabel, EmailProblemsMessage, Header, PasswordContinueMessage, PasswordDescription, PasswordLabel, PhoneProblemsMessage, ResetPasswordMessage, SendEmailCodeAgainMessage, SendPhoneCodeAgainMessage, availableSecondFactors, currentSecondFactor, handleChangeSecondFactorInput, redirectUrl, startConfirmEmailAction, startConfirmPhoneAction, userMaskedData])
🧹 Nitpick comments (1)
apps/condo/domains/user/components/auth/SecondFactorForm.tsx (1)

87-95: Consider adding error logging to empty onError handlers.

The completeConfirmEmailMutation and completeConfirmPhoneMutation have empty onError handlers. While errors appear to be handled in the response checking (lines 230-244), silent error handlers make debugging difficult.

Proposed improvement
     const [completeConfirmEmailMutation] = useCompleteConfirmEmailActionMutation({
-        onError: () => {},
+        // NOTE: Errors are handled in handleChangeSecondFactorInput response checking
+        onError: (error) => console.error('Complete confirm email action failed:', error),
     })
     // ...
     const [completeConfirmPhoneMutation] = useCompleteConfirmPhoneActionMutation({
-        onError: () => {},
+        // NOTE: Errors are handled in handleChangeSecondFactorInput response checking
+        onError: (error) => console.error('Complete confirm phone action failed:', error),
     })

Comment on lines +294 to +315
const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
// @ts-ignore
if (res?.errors?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
// @ts-ignore
const graphQLError = res?.errors?.graphQLErrors.find(isNotEnoughAuthFactors)
const authDetails = graphQLError?.extensions?.authDetails

if (authDetails?.is2FAEnabled && authDetails?.userId && authDetails?.availableSecondFactors?.length > 0) {
userIdRef.current = authDetails.userId
const availableSecondFactors = authDetails?.availableSecondFactors?.filter(factor => ['confirmPhoneToken', 'confirmEmailToken', 'password'].includes(factor)) || []
const prioritySecondFactor = currentSecondFactor || availableSecondFactors?.[0] || null

if (availableSecondFactors.length > 0) {
setUserMaskedData(authDetails?.maskedData || null)
setAvailableSecondFactors(availableSecondFactors)
setCurrentSecondFactor(prioritySecondFactor)
setStep('authenticate2FA')
}
}

return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incorrect error path for NOT_ENOUGH_AUTH_FACTORS check in mutation response.

The res.errors from Apollo's mutation result is a direct array of GraphQL errors, not an object with a graphQLErrors property. The current path res?.errors?.graphQLErrors will always be undefined, causing the 2FA flow to never trigger from the response.

🐛 Proposed fix
             const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
-            // `@ts-ignore`
-            if (res?.errors?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
-                // `@ts-ignore`
-                const graphQLError = res?.errors?.graphQLErrors.find(isNotEnoughAuthFactors)
+            if (res?.errors?.some(isNotEnoughAuthFactors)) {
+                const graphQLError = res?.errors?.find(isNotEnoughAuthFactors)
                 const authDetails = graphQLError?.extensions?.authDetails
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
// @ts-ignore
if (res?.errors?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
// @ts-ignore
const graphQLError = res?.errors?.graphQLErrors.find(isNotEnoughAuthFactors)
const authDetails = graphQLError?.extensions?.authDetails
if (authDetails?.is2FAEnabled && authDetails?.userId && authDetails?.availableSecondFactors?.length > 0) {
userIdRef.current = authDetails.userId
const availableSecondFactors = authDetails?.availableSecondFactors?.filter(factor => ['confirmPhoneToken', 'confirmEmailToken', 'password'].includes(factor)) || []
const prioritySecondFactor = currentSecondFactor || availableSecondFactors?.[0] || null
if (availableSecondFactors.length > 0) {
setUserMaskedData(authDetails?.maskedData || null)
setAvailableSecondFactors(availableSecondFactors)
setCurrentSecondFactor(prioritySecondFactor)
setStep('authenticate2FA')
}
}
return
}
const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
if (res?.errors?.some(isNotEnoughAuthFactors)) {
const graphQLError = res?.errors?.find(isNotEnoughAuthFactors)
const authDetails = graphQLError?.extensions?.authDetails
if (authDetails?.is2FAEnabled && authDetails?.userId && authDetails?.availableSecondFactors?.length > 0) {
userIdRef.current = authDetails.userId
const availableSecondFactors = authDetails?.availableSecondFactors?.filter(factor => ['confirmPhoneToken', 'confirmEmailToken', 'password'].includes(factor)) || []
const prioritySecondFactor = currentSecondFactor || availableSecondFactors?.[0] || null
if (availableSecondFactors.length > 0) {
setUserMaskedData(authDetails?.maskedData || null)
setAvailableSecondFactors(availableSecondFactors)
setCurrentSecondFactor(prioritySecondFactor)
setStep('authenticate2FA')
}
}
return
}
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/auth/RegisterForm.tsx` around lines 294 -
315, The NOT_ENOUGH_AUTH_FACTORS check is using the wrong path
(res?.errors?.graphQLErrors) so it never detects the error; update the checks to
inspect res?.errors directly (use res?.errors?.some(isNotEnoughAuthFactors) and
res?.errors?.find(isNotEnoughAuthFactors)) and then proceed to read
extensions/authDetails from that found error, keeping the existing logic that
sets userIdRef.current, setUserMaskedData, setAvailableSecondFactors,
setCurrentSecondFactor (using currentSecondFactor or availableSecondFactors[0])
and setStep('authenticate2FA'); also remove or adjust the `@ts-ignore` stubs and
ensure types align with GraphQLError[] if needed.

Comment on lines +265 to +285
const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
// @ts-ignore
if (res?.errors?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
// @ts-ignore
const graphQLError = res?.errors?.graphQLErrors.find(isNotEnoughAuthFactors)
const authDetails = graphQLError?.extensions?.authDetails

if (authDetails?.is2FAEnabled && authDetails?.userId && authDetails?.availableSecondFactors?.length > 0) {
userIdRef.current = authDetails.userId
const availableSecondFactors = authDetails?.availableSecondFactors?.filter(factor => ['confirmPhoneToken', 'confirmEmailToken'].includes(factor)) || []
const prioritySecondFactor = currentSecondFactor || availableSecondFactors?.[0] || null

if (availableSecondFactors.length > 0) {
setUserMaskedData(authDetails?.maskedData || null)
setAvailableSecondFactors(availableSecondFactors)
setConfirmActionType(prioritySecondFactor)
setIsModalVisible(false)
setIsSecondFactorModalVisible(true)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incorrect error path for NOT_ENOUGH_AUTH_FACTORS check.

Same issue as in other files - the res?.errors?.graphQLErrors path will always be undefined since Apollo mutation results return errors as a direct array.

🐛 Proposed fix
         const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
-        // `@ts-ignore`
-        if (res?.errors?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
-            // `@ts-ignore`
-            const graphQLError = res?.errors?.graphQLErrors.find(isNotEnoughAuthFactors)
+        if (res?.errors?.some(isNotEnoughAuthFactors)) {
+            const graphQLError = res?.errors?.find(isNotEnoughAuthFactors)
             const authDetails = graphQLError?.extensions?.authDetails
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/SudoTokenProvider.tsx` around lines 265 -
285, The NOT_ENOUGH_AUTH_FACTORS check is using the wrong error path
(res?.errors?.graphQLErrors) which is undefined for Apollo mutation results;
update the logic in the block using isNotEnoughAuthFactors to inspect
res?.errors (an array of GraphQLFormattedError) instead of
res?.errors?.graphQLErrors, i.e. find the matching error via
res?.errors?.find(isNotEnoughAuthFactors) and then read
graphQLError.extensions.authDetails, then proceed to set userIdRef.current,
setUserMaskedData, setAvailableSecondFactors, setConfirmActionType,
setIsModalVisible and setIsSecondFactorModalVisible exactly as before.

Comment on lines +69 to +78
const cases = [
['[email protected]', 'a***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'ab***@example.com'],
['[email protected]', 'qw***@yandex.ru'],
]
Copy link
Member

Choose a reason for hiding this comment

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

yes, but the function is named maskEmail, and not maskNormalizedEmail.. Really easy to forget to normalize and use your function. Also.. what about undefined case?


function maskPhone (normalizedPhone) {
const hasPlus = normalizedPhone.startsWith('+')
const digits = normalizedPhone.replace(/\D/g, '')
Copy link
Member

Choose a reason for hiding this comment

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

not replaceAll?

Comment on lines 80 to 91
['+79008007060', '+79*******60'],
['79008007060', '79*******60'],
['1', '1'],
['12', '1*'],
['123', '1*3'],
['1234', '1**4'],
['12345', '1***5'],
['123456', '1****6'],
['1234567', '12***67'],
['12345678', '12****78'],
['123456789', '12*****89'],
Copy link
Member

Choose a reason for hiding this comment

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

same

export const useSudoToken: UseSudoTokenContext = () => useContext(SudoTokenContext)


const NOT_NUMBER_REGEX = /\D/g
Copy link
Member

Choose a reason for hiding this comment

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

Mb move to common/constants/regex

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved

onError: authOrRegisterErrorHandler,
onError: (error) => {
if (error?.graphQLErrors?.some(gqlError => gqlError.extensions?.type === NOT_ENOUGH_AUTH_FACTORS)) {
// show flow with 2FA
Copy link
Member

Choose a reason for hiding this comment

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

mb it's better to set some state here and use it?

import { AgreementText } from './AgreementText'
import { useSecondFactor } from './SecondFactorForm'

import { updateQuery } from '../../../common/utils/helpers'
Copy link
Member

Choose a reason for hiding this comment

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

fix import

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
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

🤖 Fix all issues with AI agents
In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx`:
- Around line 229-243: In SecondFactorForm, replace the incorrect access to
res.errors.graphQLErrors by reading the first GraphQLError from the errors array
on the mutation result (res.errors?.[0]); derive the message as
firstError?.extensions?.messageForUser || firstError?.message ||
ServerErrorMessage, remove the `@ts-ignore`, and use that errorMessage in the
existing form.setFields calls (when currentSecondFactor is 'confirmPhoneToken'
or 'confirmEmailToken') so the correct error text is shown to the user.

In `@apps/condo/domains/user/components/SudoTokenProvider.tsx`:
- Around line 77-81: The external Telegram link rendered in
InstructionStepSupportTelegramChat uses SecondaryLink with target='_blank' but
lacks rel attributes; update the SecondaryLink element where it constructs the
chatBotLink (using HelpRequisites.support_bot and ChatInTelegramMessage) to
include rel='noopener noreferrer' so the opened page cannot access window.opener
and to prevent reverse-tabnabbing.
🧹 Nitpick comments (1)
apps/condo/domains/common/utils/mail.js (1)

62-75: Consider adding null/undefined guard for robustness.

If normalizedEmail is null or undefined, calling .split('@') will throw a TypeError. While the function name implies a normalized (non-null) input, adding a defensive check would prevent runtime errors if misused.

🛡️ Optional defensive guard
 function maskNormalizedEmail (normalizedEmail) {
+    if (!normalizedEmail) return normalizedEmail
+
     const [user, domain] = normalizedEmail.split('@')
     if (!domain) return normalizedEmail

Comment on lines +229 to +244
if (res?.errors) {
// @ts-ignore
const errorMessage = res.errors?.graphQLErrors?.[0]?.extensions?.messageForUser || ServerErrorMessage

if (currentSecondFactor === 'confirmPhoneToken') {
form.setFields([{
name: 'confirmPhoneCode',
errors: [errorMessage],
}])
} else if (currentSecondFactor === 'confirmEmailToken') {
form.setFields([{
name: 'confirmEmailCode',
errors: [errorMessage],
}])
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "SecondFactorForm.tsx" | head -20

Repository: open-condo-software/condo

Length of output: 131


🏁 Script executed:

cat -n apps/condo/domains/user/components/auth/SecondFactorForm.tsx | sed -n '220,250p'

Repository: open-condo-software/condo

Length of output: 1365


🌐 Web query:

Apollo Client FetchResult errors structure graphQLErrors array

💡 Result:

In Apollo Client, the FetchResult<T> you get back from a link (and that flows into mutation results, update functions, etc.) carries GraphQL execution errors in an errors array that matches the GraphQL response’s top-level errors field:

  • FetchResult.errors?: ReadonlyArray<GraphQLFormattedError> (v3+) — each entry is a formatted GraphQL error object. [1]

That same array is what Apollo Client exposes as “GraphQL errors” on the client-side error objects:

  • Apollo Client v3: ApolloError.graphQLErrors: ReadonlyArray<GraphQLFormattedError> (built from the response errors array). [2]
  • Apollo Client v4: GraphQL errors are represented as a CombinedGraphQLErrors instance; the underlying list is available as combined.errors. [3]

Structure of each entry (what’s inside the array)

Each element is a standard GraphQL error shape (commonly including):

  • message: string
  • locations?: [{ line: number, column: number }]
  • path?: (string | number)[]
  • extensions?: { ... } (often includes code, and in dev may include stacktrace-like info) [4]

Example (server response shape): an errors: [...] array of objects with message, locations, and extensions (e.g., extensions.code). [4]

Sources
[1] Apollo Client type defs showing errors?: ReadonlyArray<GraphQLFormattedError> on operation results. (app.unpkg.com)
[2] Apollo Client ApolloError type showing graphQLErrors: ReadonlyArray<GraphQLFormattedError>. (app.unpkg.com)
[3] Apollo docs for CombinedGraphQLErrors and access via .errors (v4 style). (apollographql.com)
[4] Apollo Server docs showing the GraphQL response errors array structure and extensions. (apollographql.com)

Citations:


🏁 Script executed:

cat -n apps/condo/domains/user/components/auth/SecondFactorForm.tsx | sed -n '180,230p'

Repository: open-condo-software/condo

Length of output: 2447


🏁 Script executed:

rg "confirmPhoneTokenMutation|confirmEmailTokenMutation" apps/condo/domains/user/components/auth/SecondFactorForm.tsx -B 5 -A 5

Repository: open-condo-software/condo

Length of output: 51


🏁 Script executed:

rg "completeConfirmPhoneMutation|completeConfirmEmailMutation" apps/condo/domains/user/components/auth/ -A 3

Repository: open-condo-software/condo

Length of output: 4553


🏁 Script executed:

rg "gql\`.*completeConfirmPhone\|gql\`.*completeConfirmEmail" apps/condo/domains/user/ -A 15

Repository: open-condo-software/condo

Length of output: 51


🏁 Script executed:

find apps/condo/domains/user -type f -name "*.graphql" -o -name "*mutations*" | head -20

Repository: open-condo-software/condo

Length of output: 1244


🏁 Script executed:

rg "useCompleteConfirmPhoneActionMutation|useCompleteConfirmEmailActionMutation" apps/condo/domains/user -l

Repository: open-condo-software/condo

Length of output: 253


🏁 Script executed:

cat apps/condo/domains/user/queries/CompleteConfirmPhoneAction.graphql

Repository: open-condo-software/condo

Length of output: 212


🏁 Script executed:

cat apps/condo/domains/user/queries/CompleteConfirmEmailAction.graphql

Repository: open-condo-software/condo

Length of output: 212


🏁 Script executed:

rg "useCompleteConfirmPhoneActionMutation\|useCompleteConfirmEmailActionMutation" -A 2 apps/condo/domains/user/components/auth/SecondFactorForm.tsx

Repository: open-condo-software/condo

Length of output: 51


🏁 Script executed:

cat -n apps/condo/domains/user/components/auth/SecondFactorForm.tsx | sed -n '1,50p' | grep -E "import|@ts-ignore"

Repository: open-condo-software/condo

Length of output: 1166


🏁 Script executed:

rg "graphQLErrors" apps/condo/domains/user/components/auth/SecondFactorForm.tsx

Repository: open-condo-software/condo

Length of output: 182


Fix error message extraction from mutation results.
FetchResult.errors is an array of GraphQL errors, not an object with a graphQLErrors property. The current code accessing errors.graphQLErrors always returns undefined and falls back to ServerErrorMessage. Extract the error directly from the array and remove the @ts-ignore.

🐛 Suggested fix
-            // `@ts-ignore`
-            const errorMessage = res.errors?.graphQLErrors?.[0]?.extensions?.messageForUser || ServerErrorMessage
+            const errorMessage = res.errors?.[0]?.extensions?.messageForUser || ServerErrorMessage
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (res?.errors) {
// @ts-ignore
const errorMessage = res.errors?.graphQLErrors?.[0]?.extensions?.messageForUser || ServerErrorMessage
if (currentSecondFactor === 'confirmPhoneToken') {
form.setFields([{
name: 'confirmPhoneCode',
errors: [errorMessage],
}])
} else if (currentSecondFactor === 'confirmEmailToken') {
form.setFields([{
name: 'confirmEmailCode',
errors: [errorMessage],
}])
}
if (res?.errors) {
const errorMessage = res.errors?.[0]?.extensions?.messageForUser || ServerErrorMessage
if (currentSecondFactor === 'confirmPhoneToken') {
form.setFields([{
name: 'confirmPhoneCode',
errors: [errorMessage],
}])
} else if (currentSecondFactor === 'confirmEmailToken') {
form.setFields([{
name: 'confirmEmailCode',
errors: [errorMessage],
}])
}
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx` around lines
229 - 243, In SecondFactorForm, replace the incorrect access to
res.errors.graphQLErrors by reading the first GraphQLError from the errors array
on the mutation result (res.errors?.[0]); derive the message as
firstError?.extensions?.messageForUser || firstError?.message ||
ServerErrorMessage, remove the `@ts-ignore`, and use that errorMessage in the
existing form.setFields calls (when currentSecondFactor is 'confirmPhoneToken'
or 'confirmEmailToken') so the correct error text is shown to the user.

Comment on lines +77 to +82
const InstructionStepSupportTelegramChat = intl.formatMessage({ id: 'pages.auth.validateIdentifierForm.problemsModal.instruction.steps.supportTelegramChat' }, {
chatBotLink: (
<SecondaryLink target='_blank' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
{ChatInTelegramMessage}
</SecondaryLink>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find SecondaryLink component definition
fd -a 'SecondaryLink' -E node_modules | head -20

Repository: open-condo-software/condo

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Search for SecondaryLink in the codebase more broadly
rg 'export.*SecondaryLink|const SecondaryLink|function SecondaryLink' -E --max-count=20

Repository: open-condo-software/condo

Length of output: 475


🏁 Script executed:

#!/bin/bash
# Check if SecondaryLink is from `@open-condo/ui`
rg "from ['\"@open-condo/ui" --max-count=50 | grep -i link

Repository: open-condo-software/condo

Length of output: 201


🏁 Script executed:

cat -n /home/jailuser/git/apps/condo/domains/user/components/auth/SecondaryLink.tsx

Repository: open-condo-software/condo

Length of output: 510


🏁 Script executed:

rg 'rel' /home/jailuser/git/apps/condo/domains/user/components/auth/SecondaryLink.tsx -A 2 -B 2

Repository: open-condo-software/condo

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Search for other uses of target="_blank" in the codebase to see the pattern
rg "target=['\"]_blank['\"]" -A 2 -B 2 --max-count=20

Repository: open-condo-software/condo

Length of output: 38151


🏁 Script executed:

#!/bin/bash
# Check if rel is typically added alongside target="_blank"
rg "target=['\"]_blank['\"].*rel=" --max-count=20

Repository: open-condo-software/condo

Length of output: 1672


Add rel='noopener noreferrer' to prevent reverse-tabnabbing.

The external Telegram link opens with target='_blank' but lacks the rel attribute. SecondaryLink does not automatically inject rel, so add it here.

Proposed fix
-            <SecondaryLink target='_blank' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
+            <SecondaryLink target='_blank' rel='noopener noreferrer' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const InstructionStepSupportTelegramChat = intl.formatMessage({ id: 'pages.auth.validateIdentifierForm.problemsModal.instruction.steps.supportTelegramChat' }, {
chatBotLink: (
<SecondaryLink target='_blank' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
{ChatInTelegramMessage}
</SecondaryLink>
const InstructionStepSupportTelegramChat = intl.formatMessage({ id: 'pages.auth.validateIdentifierForm.problemsModal.instruction.steps.supportTelegramChat' }, {
chatBotLink: (
<SecondaryLink target='_blank' rel='noopener noreferrer' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
{ChatInTelegramMessage}
</SecondaryLink>
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/SudoTokenProvider.tsx` around lines 77 -
81, The external Telegram link rendered in InstructionStepSupportTelegramChat
uses SecondaryLink with target='_blank' but lacks rel attributes; update the
SecondaryLink element where it constructs the chatBotLink (using
HelpRequisites.support_bot and ChatInTelegramMessage) to include rel='noopener
noreferrer' so the opened page cannot access window.opener and to prevent
reverse-tabnabbing.

@Alllex202 Alllex202 force-pushed the feat/condo/DOMA-12564/2fa-frontend branch from c7ead17 to a2f5bad Compare January 27, 2026 14:07
Copy link
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: 9

🤖 Fix all issues with AI agents
In `@apps/condo/domains/user/schema/ConfirmPhoneActionService.js`:
- Around line 266-276: The code currently assigns phone = user?.phone || null
which causes normalizePhone(null) to trigger ERRORS.WRONG_PHONE_FORMAT; instead,
after fetching the user in ConfirmPhoneActionService (check userFromInput and
User.getOne), add an explicit check for a missing phone (user.phone === null or
undefined) and throw a clearer GQLError (e.g. ERRORS.USER_HAS_NO_PHONE) before
calling normalizePhone; update the error enum/constants to include
USER_HAS_NO_PHONE if needed and ensure you still fall back to phoneFromInput
when no user ID is provided.

In `@apps/condo/lang/en/en.json`:
- Around line 743-755: Replace the inconsistent "Second factor" phrasing with
"Two-factor authentication" across the SecondFactorForm keys: update
component.SecondFactorForm.title,
component.SecondFactorForm.title.forAuthorization, and any user-facing
labels/descriptions under component.SecondFactorForm.type.* to use "Two-factor
authentication" (or "Two-factor authentication for authorization" where context
requires) while keeping placeholders like {email}/{phone} and button text (e.g.,
"Send code again", "Continue") unchanged; make the JSON value edits for these
keys so wording matches the profile section's "Two-factor authentication"
terminology.
- Around line 2453-2454: The JSON string for the key
"pages.condo.profile.2FA.tooltip" contains unescaped double quotes around Edit
and Information which breaks parsing; update that value so the internal quotes
are escaped (e.g., change "click "Edit" in the "Information" tab" to "click
\"Edit\" in the \"Information\" tab") or replace the inner quotes with single
quotes, ensuring the string remains valid JSON and referencing the
"pages.condo.profile.2FA.tooltip" key.

In `@apps/condo/lang/es/es.json`:
- Around line 556-557: Update the Spanish strings for the two 2FA keys to use
consistent terminology: replace "api.user.NOT_ENOUGH_AUTH_FACTORS" value with
wording matching the existing INSUFFICIENT_AUTHENTICATION_FACTORS phrasing
(e.g., "Factores de autenticación insuficientes") and change
"api.user.authenticateOrRegisterUserWithToken.UNEXPECTED_SECOND_FACTOR" to use
"segundo factor" instead of "segundo nivel" (e.g., "Factor inesperado: segundo
factor"). Ensure both keys use the same noun choice ("factor"/"factores") and
similar adjective ordering for consistency in the UI.
- Around line 759-762: The two i18n keys
"component.SudoTokenProvider.modal.password.title.disable2FA" and
"component.SudoTokenProvider.modal.password.title.enable2FA" use the verb
“ingresar” while the rest of the modal strings use “introducir”; update the
values of those two keys to use “introducir” (e.g., replace “necesitas ingresar
una contraseña.” with “necesitas introducir una contraseña.”) so the verb choice
is consistent with "component.SudoTokenProvider.modal.password.label" and
"component.SudoTokenProvider.modal.password.title".
- Around line 2453-2454: The tooltip string for key
pages.condo.profile.2FA.tooltip uses formal "usted" but the app uses informal
"tú"; update the value to use informal phrasing (e.g., "Para permitirnos
enviarte el código, por favor proporciona tus {factors}. Para hacerlo, haz clic
en \"Editar\" en la pestaña \"Información\" y añade cualquier información que
falte.") so it matches the UI tone while preserving the {factors} placeholder
and escaped quotes.
- Around line 743-755: Update the 2FA copy to use the informal tone
consistently: change
"component.SecondFactorForm.type.confirmPhoneToken.description" from "Ingrese el
código enviado a {phone}" to use the informal verb "Introduce..." and change
"component.SecondFactorForm.type.password.description" from "Introduzca la
contraseña" to "Introduce la contraseña"; keep placeholders ({phone}, etc.)
unchanged and leave the existing labels
(component.SecondFactorForm.type.confirmPhoneToken.label,
component.SecondFactorForm.type.password.label) as-is.

In `@apps/condo/lang/ru/ru.json`:
- Around line 759-762: The Russian strings for keys
component.SudoTokenProvider.modal.password.title.disable2FA and
component.SudoTokenProvider.modal.password.title.enable2FA are missing a comma
after “авторизацию”; update those two values to insert a comma after
“авторизацию” (i.e., "Чтобы выключить двухфакторную авторизацию, требуется
ввести пароль" and "Чтобы включить двухфакторную авторизацию, требуется ввести
пароль") so the subordinate clause is punctuated correctly.

In `@apps/condo/pages/auth/change-password.tsx`:
- Around line 472-476: The useEffect listening for step ===
'getSudoTokenBeforeChangePassword' uses onSubmitWithSecondFactor but doesn't
include it in the dependency array; either add onSubmitWithSecondFactor to the
dependency array of the effect or memoize onSubmitWithSecondFactor with
useCallback so the effect won't capture a stale closure—update the effect to use
[step, onSubmitWithSecondFactor] (or wrap the onSubmitWithSecondFactor
definition in useCallback and keep the dependency array as [step]) ensuring
references to onSubmitWithSecondFactor and step are consistent.
🧹 Nitpick comments (5)
apps/condo/domains/user/schema/ConfirmEmailActionService.js (2)

86-99: Document the new validation errors in the mutation docs.
These errors are now thrown but aren’t listed in the startConfirmEmailAction doc block. Consider adding them to improve GraphiQL docs and client expectations.

Proposed doc update
             errors: {
                 ...pick(ERRORS, [
                     DV_VERSION_MISMATCH,
                     'WRONG_SENDER_FORMAT',
                     CAPTCHA_CHECK_FAILED,
                     UNABLE_TO_FIND_CONFIRM_EMAIL_ACTION,
                     'WRONG_EMAIL_FORMAT',
                     'GENERATE_TOKEN_ERROR',
+                    'EMAIL_AND_USER_ID_IS_MISSING',
+                    'SHOULD_BE_ONE_IDENTIFIER_ONLY',
                 ]),
             },

287-327: Consider a clearer error when userId doesn’t resolve to an email.
Right now a missing user/email falls through to WRONG_EMAIL_FORMAT, which is confusing for userId-based calls. A dedicated error (or mapping to a “user not found / missing email” case) would be clearer.

apps/condo/pages/auth/change-password.tsx (2)

551-559: Use JSX children instead of the children prop.

The static analysis tool correctly flagged that passing children via a prop is not the canonical React pattern.

♻️ Proposed fix
                             <Button
                                 key='submit'
                                 type='primary'
                                 loading={isLoading}
                                 htmlType='submit'
-                                children={`${SaveMsg} ${AndSignInMessage}`}
                                 block
-                            />
+                            >
+                                {`${SaveMsg} ${AndSignInMessage}`}
+                            </Button>

159-164: Consider extracting duplicate second factor mapping logic.

The same conditional mapping for secondFactor based on currentSecondFactor type is repeated for both email (lines 159-164) and phone (lines 178-183) authentication paths. This could be extracted into a helper function.

♻️ Example helper extraction
// Helper to build second factor input
const buildSecondFactor = <T extends { ConfirmEmailToken: string, ConfirmPhoneToken: string }>(
    secondFactorData: { currentSecondFactor?: 'confirmEmailToken' | 'confirmPhoneToken', confirmToken: string } | undefined,
    types: T
): { value: string, type: T['ConfirmEmailToken'] | T['ConfirmPhoneToken'] } | undefined => {
    if (!secondFactorData) return undefined
    if (secondFactorData.currentSecondFactor === 'confirmEmailToken') {
        return { value: secondFactorData.confirmToken, type: types.ConfirmEmailToken }
    }
    if (secondFactorData.currentSecondFactor === 'confirmPhoneToken') {
        return { value: secondFactorData.confirmToken, type: types.ConfirmPhoneToken }
    }
    return undefined
}
apps/condo/lang/ru/ru.json (1)

556-561: Unify 2FA terminology for consistency.

Here it’s “факторов авторизации”, while existing 2FA strings nearby use “факторов аутентификации”. Consider aligning to avoid mixed terminology in user-facing errors.

♻️ Suggested wording alignment
-"api.user.NOT_ENOUGH_AUTH_FACTORS": "Недостаточно факторов авторизации",
+"api.user.NOT_ENOUGH_AUTH_FACTORS": "Недостаточно факторов аутентификации",

Comment on lines +266 to 276
if (userFromInput?.id) {
const user = await User.getOne(context, { id: userFromInput.id, deletedAt: null }, 'id phone')
phone = user?.phone || null
} else {
phone = phoneFromInput
}

const normalizedPhone = normalizePhone(phone)
if (!normalizedPhone) {
throw new GQLError(ERRORS.WRONG_PHONE_FORMAT, context)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

User without phone returns null, leading to unclear error.

If a user exists but has no phone number set (user.phone is null/undefined), the code will proceed with phone = null, and later normalizePhone(null) will return a falsy value, throwing WRONG_PHONE_FORMAT. This error message is misleading when the actual issue is that the user has no phone.

🔧 Proposed fix - add explicit check for missing phone
                 if (userFromInput?.id) {
                     const user = await User.getOne(context, { id: userFromInput.id, deletedAt: null }, 'id phone')
-                    phone = user?.phone || null
+                    if (!user) {
+                        throw new GQLError({ ...ERRORS.WRONG_PHONE_FORMAT, message: 'User not found' }, context)
+                    }
+                    if (!user.phone) {
+                        throw new GQLError({ ...ERRORS.WRONG_PHONE_FORMAT, message: 'User has no phone number' }, context)
+                    }
+                    phone = user.phone
                 } else {
                     phone = phoneFromInput
                 }

Alternatively, consider adding a dedicated error type like USER_HAS_NO_PHONE for clarity.

🤖 Prompt for AI Agents
In `@apps/condo/domains/user/schema/ConfirmPhoneActionService.js` around lines 266
- 276, The code currently assigns phone = user?.phone || null which causes
normalizePhone(null) to trigger ERRORS.WRONG_PHONE_FORMAT; instead, after
fetching the user in ConfirmPhoneActionService (check userFromInput and
User.getOne), add an explicit check for a missing phone (user.phone === null or
undefined) and throw a clearer GQLError (e.g. ERRORS.USER_HAS_NO_PHONE) before
calling normalizePhone; update the error enum/constants to include
USER_HAS_NO_PHONE if needed and ensure you still fall back to phoneFromInput
when no user ID is provided.

Comment on lines +743 to +756
"component.SecondFactorForm.title": "Second factor",
"component.SecondFactorForm.title.forAuthorization": "Second factor of authorization",
"component.SecondFactorForm.type.confirmEmailToken.description": "Enter the code sent to {email}",
"component.SecondFactorForm.type.confirmEmailToken.label": "By email",
"component.SecondFactorForm.type.confirmEmailToken.problems": "I haven't received the email with the code",
"component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Send code again",
"component.SecondFactorForm.type.confirmPhoneToken.description": "Enter the code sent to {phone}",
"component.SecondFactorForm.type.confirmPhoneToken.label": "By phone",
"component.SecondFactorForm.type.confirmPhoneToken.problems": "I haven't received an SMS with a code",
"component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Send code again",
"component.SecondFactorForm.type.password.description": "Enter password",
"component.SecondFactorForm.type.password.label": "By password",
"component.SecondFactorForm.type.password.continue": "Continue",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align “Second factor” wording with the “Two‑factor authentication” terminology.

This section currently uses “Second factor” while the profile section uses “Two-factor authentication.” Consider consistent phrasing across UI.

✏️ Suggested copy update
-  "component.SecondFactorForm.title": "Second factor",
-  "component.SecondFactorForm.title.forAuthorization": "Second factor of authorization",
+  "component.SecondFactorForm.title": "Two-factor authentication",
+  "component.SecondFactorForm.title.forAuthorization": "Two-factor authentication for sign-in",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"component.SecondFactorForm.title": "Second factor",
"component.SecondFactorForm.title.forAuthorization": "Second factor of authorization",
"component.SecondFactorForm.type.confirmEmailToken.description": "Enter the code sent to {email}",
"component.SecondFactorForm.type.confirmEmailToken.label": "By email",
"component.SecondFactorForm.type.confirmEmailToken.problems": "I haven't received the email with the code",
"component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Send code again",
"component.SecondFactorForm.type.confirmPhoneToken.description": "Enter the code sent to {phone}",
"component.SecondFactorForm.type.confirmPhoneToken.label": "By phone",
"component.SecondFactorForm.type.confirmPhoneToken.problems": "I haven't received an SMS with a code",
"component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Send code again",
"component.SecondFactorForm.type.password.description": "Enter password",
"component.SecondFactorForm.type.password.label": "By password",
"component.SecondFactorForm.type.password.continue": "Continue",
"component.SecondFactorForm.title": "Two-factor authentication",
"component.SecondFactorForm.title.forAuthorization": "Two-factor authentication for sign-in",
"component.SecondFactorForm.type.confirmEmailToken.description": "Enter the code sent to {email}",
"component.SecondFactorForm.type.confirmEmailToken.label": "By email",
"component.SecondFactorForm.type.confirmEmailToken.problems": "I haven't received the email with the code",
"component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Send code again",
"component.SecondFactorForm.type.confirmPhoneToken.description": "Enter the code sent to {phone}",
"component.SecondFactorForm.type.confirmPhoneToken.label": "By phone",
"component.SecondFactorForm.type.confirmPhoneToken.problems": "I haven't received an SMS with a code",
"component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Send code again",
"component.SecondFactorForm.type.password.description": "Enter password",
"component.SecondFactorForm.type.password.label": "By password",
"component.SecondFactorForm.type.password.continue": "Continue",
🤖 Prompt for AI Agents
In `@apps/condo/lang/en/en.json` around lines 743 - 755, Replace the inconsistent
"Second factor" phrasing with "Two-factor authentication" across the
SecondFactorForm keys: update component.SecondFactorForm.title,
component.SecondFactorForm.title.forAuthorization, and any user-facing
labels/descriptions under component.SecondFactorForm.type.* to use "Two-factor
authentication" (or "Two-factor authentication for authorization" where context
requires) while keeping placeholders like {email}/{phone} and button text (e.g.,
"Send code again", "Continue") unchanged; make the JSON value edits for these
keys so wording matches the profile section's "Two-factor authentication"
terminology.

Comment on lines +556 to +558
"api.user.NOT_ENOUGH_AUTH_FACTORS": "No hay suficientes factores de autenticación",
"api.user.authenticateOrRegisterUserWithToken.UNEXPECTED_SECOND_FACTOR": "Factor inesperado de segundo nivel",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align 2FA error phrasing for consistency.

Line 556 uses different wording than the existing “INSUFFICIENT_AUTHENTICATION_FACTORS”, and Line 557 says “segundo nivel” while the UI uses “segundo factor”. Consider unifying terminology to avoid mixed messages.

✏️ Suggested update
-  "api.user.NOT_ENOUGH_AUTH_FACTORS": "No hay suficientes factores de autenticación",
-  "api.user.authenticateOrRegisterUserWithToken.UNEXPECTED_SECOND_FACTOR": "Factor inesperado de segundo nivel",
+  "api.user.NOT_ENOUGH_AUTH_FACTORS": "Factores de autenticación insuficientes",
+  "api.user.authenticateOrRegisterUserWithToken.UNEXPECTED_SECOND_FACTOR": "Segundo factor inesperado",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"api.user.NOT_ENOUGH_AUTH_FACTORS": "No hay suficientes factores de autenticación",
"api.user.authenticateOrRegisterUserWithToken.UNEXPECTED_SECOND_FACTOR": "Factor inesperado de segundo nivel",
"api.user.NOT_ENOUGH_AUTH_FACTORS": "Factores de autenticación insuficientes",
"api.user.authenticateOrRegisterUserWithToken.UNEXPECTED_SECOND_FACTOR": "Segundo factor inesperado",
🤖 Prompt for AI Agents
In `@apps/condo/lang/es/es.json` around lines 556 - 557, Update the Spanish
strings for the two 2FA keys to use consistent terminology: replace
"api.user.NOT_ENOUGH_AUTH_FACTORS" value with wording matching the existing
INSUFFICIENT_AUTHENTICATION_FACTORS phrasing (e.g., "Factores de autenticación
insuficientes") and change
"api.user.authenticateOrRegisterUserWithToken.UNEXPECTED_SECOND_FACTOR" to use
"segundo factor" instead of "segundo nivel" (e.g., "Factor inesperado: segundo
factor"). Ensure both keys use the same noun choice ("factor"/"factores") and
similar adjective ordering for consistency in the UI.

Comment on lines +743 to +756
"component.SecondFactorForm.title": "Segundo factor",
"component.SecondFactorForm.title.forAuthorization": "Segundo factor de autorización",
"component.SecondFactorForm.type.confirmEmailToken.description": "Introduce el código enviado a {email}",
"component.SecondFactorForm.type.confirmEmailToken.label": "Por email",
"component.SecondFactorForm.type.confirmEmailToken.problems": "No he recibido el correo electrónico con el código",
"component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Enviar el código de nuevo",
"component.SecondFactorForm.type.confirmPhoneToken.description": "Ingrese el código enviado a {phone}",
"component.SecondFactorForm.type.confirmPhoneToken.label": "Por teléfono",
"component.SecondFactorForm.type.confirmPhoneToken.problems": "No he recibido un SMS con un código",
"component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Enviar el código de nuevo",
"component.SecondFactorForm.type.password.description": "Introduzca la contraseña",
"component.SecondFactorForm.type.password.label": "Por contraseña",
"component.SecondFactorForm.type.password.continue": "Continuar",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize tone and terminology in 2FA form strings.

Lines 746, 749, and 753 mix “email/telefono” and formal verbs (“Ingrese/Introduzca”) with mostly informal copy elsewhere. Suggest aligning to the prevalent informal tone.

✏️ Suggested update
-  "component.SecondFactorForm.type.confirmEmailToken.label": "Por email",
+  "component.SecondFactorForm.type.confirmEmailToken.label": "Por correo electrónico",
-  "component.SecondFactorForm.type.confirmPhoneToken.description": "Ingrese el código enviado a {phone}",
+  "component.SecondFactorForm.type.confirmPhoneToken.description": "Introduce el código enviado a {phone}",
-  "component.SecondFactorForm.type.confirmPhoneToken.problems": "No he recibido un SMS con un código",
+  "component.SecondFactorForm.type.confirmPhoneToken.problems": "No he recibido el SMS con el código",
-  "component.SecondFactorForm.type.password.description": "Introduzca la contraseña",
+  "component.SecondFactorForm.type.password.description": "Introduce la contraseña",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"component.SecondFactorForm.title": "Segundo factor",
"component.SecondFactorForm.title.forAuthorization": "Segundo factor de autorización",
"component.SecondFactorForm.type.confirmEmailToken.description": "Introduce el código enviado a {email}",
"component.SecondFactorForm.type.confirmEmailToken.label": "Por email",
"component.SecondFactorForm.type.confirmEmailToken.problems": "No he recibido el correo electrónico con el código",
"component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Enviar el código de nuevo",
"component.SecondFactorForm.type.confirmPhoneToken.description": "Ingrese el código enviado a {phone}",
"component.SecondFactorForm.type.confirmPhoneToken.label": "Por teléfono",
"component.SecondFactorForm.type.confirmPhoneToken.problems": "No he recibido un SMS con un código",
"component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Enviar el código de nuevo",
"component.SecondFactorForm.type.password.description": "Introduzca la contraseña",
"component.SecondFactorForm.type.password.label": "Por contraseña",
"component.SecondFactorForm.type.password.continue": "Continuar",
"component.SecondFactorForm.title": "Segundo factor",
"component.SecondFactorForm.title.forAuthorization": "Segundo factor de autorización",
"component.SecondFactorForm.type.confirmEmailToken.description": "Introduce el código enviado a {email}",
"component.SecondFactorForm.type.confirmEmailToken.label": "Por correo electrónico",
"component.SecondFactorForm.type.confirmEmailToken.problems": "No he recibido el correo electrónico con el código",
"component.SecondFactorForm.type.confirmEmailToken.sendCodeAgain": "Enviar el código de nuevo",
"component.SecondFactorForm.type.confirmPhoneToken.description": "Introduce el código enviado a {phone}",
"component.SecondFactorForm.type.confirmPhoneToken.label": "Por teléfono",
"component.SecondFactorForm.type.confirmPhoneToken.problems": "No he recibido el SMS con el código",
"component.SecondFactorForm.type.confirmPhoneToken.sendCodeAgain": "Enviar el código de nuevo",
"component.SecondFactorForm.type.password.description": "Introduce la contraseña",
"component.SecondFactorForm.type.password.label": "Por contraseña",
"component.SecondFactorForm.type.password.continue": "Continuar",
🤖 Prompt for AI Agents
In `@apps/condo/lang/es/es.json` around lines 743 - 755, Update the 2FA copy to
use the informal tone consistently: change
"component.SecondFactorForm.type.confirmPhoneToken.description" from "Ingrese el
código enviado a {phone}" to use the informal verb "Introduce..." and change
"component.SecondFactorForm.type.password.description" from "Introduzca la
contraseña" to "Introduce la contraseña"; keep placeholders ({phone}, etc.)
unchanged and leave the existing labels
(component.SecondFactorForm.type.confirmPhoneToken.label,
component.SecondFactorForm.type.password.label) as-is.

Comment on lines +759 to +763
"component.SudoTokenProvider.modal.password.label": "Introduzca la contraseña",
"component.SudoTokenProvider.modal.password.title": "Introduzca la contraseña",
"component.SudoTokenProvider.modal.password.title.disable2FA": "Para desactivar la autenticación de dos factores, necesitas ingresar una contraseña.",
"component.SudoTokenProvider.modal.password.title.enable2FA": "Para habilitar la autenticación de dos factores, necesitas ingresar una contraseña.",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use consistent verb choice in 2FA enable/disable modal.

Lines 761-762 use “ingresar”, while nearby strings use “introducir”. Consider aligning to “introducir” for consistency.

✏️ Suggested update
-  "component.SudoTokenProvider.modal.password.title.disable2FA": "Para desactivar la autenticación de dos factores, necesitas ingresar una contraseña.",
-  "component.SudoTokenProvider.modal.password.title.enable2FA": "Para habilitar la autenticación de dos factores, necesitas ingresar una contraseña.",
+  "component.SudoTokenProvider.modal.password.title.disable2FA": "Para desactivar la autenticación de dos factores, necesitas introducir la contraseña.",
+  "component.SudoTokenProvider.modal.password.title.enable2FA": "Para habilitar la autenticación de dos factores, necesitas introducir la contraseña.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"component.SudoTokenProvider.modal.password.label": "Introduzca la contraseña",
"component.SudoTokenProvider.modal.password.title": "Introduzca la contraseña",
"component.SudoTokenProvider.modal.password.title.disable2FA": "Para desactivar la autenticación de dos factores, necesitas ingresar una contraseña.",
"component.SudoTokenProvider.modal.password.title.enable2FA": "Para habilitar la autenticación de dos factores, necesitas ingresar una contraseña.",
"component.SudoTokenProvider.modal.password.label": "Introduzca la contraseña",
"component.SudoTokenProvider.modal.password.title": "Introduzca la contraseña",
"component.SudoTokenProvider.modal.password.title.disable2FA": "Para desactivar la autenticación de dos factores, necesitas introducir la contraseña.",
"component.SudoTokenProvider.modal.password.title.enable2FA": "Para habilitar la autenticación de dos factores, necesitas introducir la contraseña.",
🤖 Prompt for AI Agents
In `@apps/condo/lang/es/es.json` around lines 759 - 762, The two i18n keys
"component.SudoTokenProvider.modal.password.title.disable2FA" and
"component.SudoTokenProvider.modal.password.title.enable2FA" use the verb
“ingresar” while the rest of the modal strings use “introducir”; update the
values of those two keys to use “introducir” (e.g., replace “necesitas ingresar
una contraseña.” with “necesitas introducir una contraseña.”) so the verb choice
is consistent with "component.SudoTokenProvider.modal.password.label" and
"component.SudoTokenProvider.modal.password.title".

Comment on lines 2453 to 2454
"pages.condo.profile.2FA.title": "Autenticación de dos factores",
"pages.condo.profile.2FA.tooltip": "Para permitirnos enviarle el código, por favor proporcione sus {factors}. Para hacerlo, haga clic en \"Editar\" en la pestaña \"Información\" y agregue cualquier información que falte.",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Match informal tone in the profile 2FA tooltip.

Line 2454 is in formal “usted” while most UI strings use “tú”. Consider switching to informal for consistency.

✏️ Suggested update
-  "pages.condo.profile.2FA.tooltip": "Para permitirnos enviarle el código, por favor proporcione sus {factors}. Para hacerlo, haga clic en \"Editar\" en la pestaña \"Información\" y agregue cualquier información que falte.",
+  "pages.condo.profile.2FA.tooltip": "Para poder enviarte el código, por favor proporciona tus {factors}. Para hacerlo, haz clic en \"Editar\" en la pestaña \"Información\" y añade la información que falte.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"pages.condo.profile.2FA.title": "Autenticación de dos factores",
"pages.condo.profile.2FA.tooltip": "Para permitirnos enviarle el código, por favor proporcione sus {factors}. Para hacerlo, haga clic en \"Editar\" en la pestaña \"Información\" y agregue cualquier información que falte.",
"pages.condo.profile.2FA.title": "Autenticación de dos factores",
"pages.condo.profile.2FA.tooltip": "Para poder enviarte el código, por favor proporciona tus {factors}. Para hacerlo, haz clic en \"Editar\" en la pestaña \"Información\" y añade la información que falte.",
🤖 Prompt for AI Agents
In `@apps/condo/lang/es/es.json` around lines 2453 - 2454, The tooltip string for
key pages.condo.profile.2FA.tooltip uses formal "usted" but the app uses
informal "tú"; update the value to use informal phrasing (e.g., "Para
permitirnos enviarte el código, por favor proporciona tus {factors}. Para
hacerlo, haz clic en \"Editar\" en la pestaña \"Información\" y añade cualquier
información que falte.") so it matches the UI tone while preserving the
{factors} placeholder and escaped quotes.

Comment on lines +759 to +763
"component.SudoTokenProvider.modal.password.label": "Введите пароль",
"component.SudoTokenProvider.modal.password.title": "Введите пароль",
"component.SudoTokenProvider.modal.password.title.disable2FA": "Чтобы выключить двухфакторную авторизацию требуется ввести пароль",
"component.SudoTokenProvider.modal.password.title.enable2FA": "Чтобы включить двухфакторную авторизацию требуется ввести пароль",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add missing comma in subordinate clause.

In Russian, a comma is needed after “авторизацию” in these sentences.

✏️ Proposed fix
-"component.SudoTokenProvider.modal.password.title.disable2FA": "Чтобы выключить двухфакторную авторизацию требуется ввести пароль",
-"component.SudoTokenProvider.modal.password.title.enable2FA": "Чтобы включить двухфакторную авторизацию требуется ввести пароль",
+"component.SudoTokenProvider.modal.password.title.disable2FA": "Чтобы выключить двухфакторную авторизацию, требуется ввести пароль",
+"component.SudoTokenProvider.modal.password.title.enable2FA": "Чтобы включить двухфакторную авторизацию, требуется ввести пароль",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"component.SudoTokenProvider.modal.password.label": "Введите пароль",
"component.SudoTokenProvider.modal.password.title": "Введите пароль",
"component.SudoTokenProvider.modal.password.title.disable2FA": "Чтобы выключить двухфакторную авторизацию требуется ввести пароль",
"component.SudoTokenProvider.modal.password.title.enable2FA": "Чтобы включить двухфакторную авторизацию требуется ввести пароль",
"component.SudoTokenProvider.modal.password.label": "Введите пароль",
"component.SudoTokenProvider.modal.password.title": "Введите пароль",
"component.SudoTokenProvider.modal.password.title.disable2FA": "Чтобы выключить двухфакторную авторизацию, требуется ввести пароль",
"component.SudoTokenProvider.modal.password.title.enable2FA": "Чтобы включить двухфакторную авторизацию, требуется ввести пароль",
🤖 Prompt for AI Agents
In `@apps/condo/lang/ru/ru.json` around lines 759 - 762, The Russian strings for
keys component.SudoTokenProvider.modal.password.title.disable2FA and
component.SudoTokenProvider.modal.password.title.enable2FA are missing a comma
after “авторизацию”; update those two values to insert a comma after
“авторизацию” (i.e., "Чтобы выключить двухфакторную авторизацию, требуется
ввести пароль" and "Чтобы включить двухфакторную авторизацию, требуется ввести
пароль") so the subordinate clause is punctuated correctly.

Comment on lines +472 to +476
useEffect(() => {
if (step !== 'getSudoTokenBeforeChangePassword') return

onSubmitWithSecondFactor()
}, [step])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing dependency in useEffect may cause stale closure.

The onSubmitWithSecondFactor function is called inside the effect but is not listed in the dependency array. This could lead to stale closure issues if the callback changes.

🔧 Proposed fix
     useEffect(() => {
         if (step !== 'getSudoTokenBeforeChangePassword') return

         onSubmitWithSecondFactor()
-    }, [step])
+    }, [step, onSubmitWithSecondFactor])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (step !== 'getSudoTokenBeforeChangePassword') return
onSubmitWithSecondFactor()
}, [step])
useEffect(() => {
if (step !== 'getSudoTokenBeforeChangePassword') return
onSubmitWithSecondFactor()
}, [step, onSubmitWithSecondFactor])
🤖 Prompt for AI Agents
In `@apps/condo/pages/auth/change-password.tsx` around lines 472 - 476, The
useEffect listening for step === 'getSudoTokenBeforeChangePassword' uses
onSubmitWithSecondFactor but doesn't include it in the dependency array; either
add onSubmitWithSecondFactor to the dependency array of the effect or memoize
onSubmitWithSecondFactor with useCallback so the effect won't capture a stale
closure—update the effect to use [step, onSubmitWithSecondFactor] (or wrap the
onSubmitWithSecondFactor definition in useCallback and keep the dependency array
as [step]) ensuring references to onSubmitWithSecondFactor and step are
consistent.

Copy link
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: 5

🤖 Fix all issues with AI agents
In `@apps/condo/domains/common/components/Form/FormItem.ts`:
- Line 11: Replace the invalid CSS declaration "white-space: wrap" in
FormItem.ts with a valid value; locate the styled rule or CSS block that sets
"white-space" (search for "white-space: wrap" in FormItem.ts) and change it to
"white-space: normal" (or "white-space: pre-wrap" if you need to preserve
existing line breaks while allowing wrapping) so the label text wraps correctly
in browsers.

In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx`:
- Around line 114-184: Both startConfirmEmailAction and startConfirmPhoneAction
currently set the token expiry before performing the mutation, which blocks
resends even when the mutation fails; remove the pre-mutation
setConfirmEmailTokenExpiredDate/setConfirmPhoneTokenExpiredDate calls and
instead set the expiry only on success (after you obtain token from
res?.data?.result?.token, e.g. inside the if (token) block) so failures do not
start the cooldown; keep the initial shouldWait check, captcha execution
(executeCaptcha), and mutations
(startConfirmEmailActionMutation/startConfirmPhoneActionMutation) unchanged
otherwise.
- Around line 57-61: The external Telegram link in the
InstructionStepSupportTelegramChat JSX uses target='_blank' without rel
attributes; update the SecondaryLink element (used in
InstructionStepSupportTelegramChat) to include rel='noopener noreferrer' when
rendering the href built from HelpRequisites?.support_bot and
ChatInTelegramMessage so the external link is safe from reverse-tabnabbing.

In `@apps/condo/domains/user/components/SudoTokenProvider.tsx`:
- Around line 354-367: The code assumes res.errors has a graphQLErrors property
which is wrong; update the extraction to read the first error from the errors
array and fallback to its extension or message before using ServerErrorMessage.
Change the errorMessage assignment to something like: const errorMessage =
res.errors?.[0]?.extensions?.messageForUser || res.errors?.[0]?.message ||
ServerErrorMessage (remove the `@ts-ignore`), leaving the existing handling for
currentSecondFactor and form.setFields intact so
confirmPhoneCode/confirmEmailCode get the proper message.

In `@apps/condo/lang/ru/ru.json`:
- Around line 2454-2457: Update the Russian translation value for the key
pages.condo.profile.2FA.tooltip.notVerifiedPhone to correct the typo: replace
"Чтобы мы могли присылать вам код, подтвердите ваш номер телефон" with "Чтобы мы
могли присылать вам код, подтвердите ваш номер телефона" so the phrase uses the
correct genitive form; leave the rest of the JSON key/value intact.

label {
height: auto;
white-space: nowrap;
white-space: wrap;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Invalid CSS value: wrap is not a valid white-space property value.

white-space: wrap is not a recognized CSS value. The valid values are normal, nowrap, pre, pre-wrap, pre-line, and break-spaces. This declaration will be ignored by browsers, and the label wrapping behavior will not work as intended.

To allow text to wrap normally, use white-space: normal:

🐛 Proposed fix
     label {
       height: auto;
-      white-space: wrap;
+      white-space: normal;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
white-space: wrap;
label {
height: auto;
white-space: normal;
}
🤖 Prompt for AI Agents
In `@apps/condo/domains/common/components/Form/FormItem.ts` at line 11, Replace
the invalid CSS declaration "white-space: wrap" in FormItem.ts with a valid
value; locate the styled rule or CSS block that sets "white-space" (search for
"white-space: wrap" in FormItem.ts) and change it to "white-space: normal" (or
"white-space: pre-wrap" if you need to preserve existing line breaks while
allowing wrapping) so the label text wraps correctly in browsers.

Comment on lines +57 to +61
const InstructionStepSupportTelegramChat = intl.formatMessage({ id: 'pages.auth.validateIdentifierForm.problemsModal.instruction.steps.supportTelegramChat' }, {
chatBotLink: (
<SecondaryLink target='_blank' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
{ChatInTelegramMessage}
</SecondaryLink>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add rel='noopener noreferrer' to the external Telegram link.

Opening a new tab without rel allows reverse-tabnabbing.

🛡️ Proposed fix
-            <SecondaryLink target='_blank' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
+            <SecondaryLink target='_blank' rel='noopener noreferrer' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const InstructionStepSupportTelegramChat = intl.formatMessage({ id: 'pages.auth.validateIdentifierForm.problemsModal.instruction.steps.supportTelegramChat' }, {
chatBotLink: (
<SecondaryLink target='_blank' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
{ChatInTelegramMessage}
</SecondaryLink>
const InstructionStepSupportTelegramChat = intl.formatMessage({ id: 'pages.auth.validateIdentifierForm.problemsModal.instruction.steps.supportTelegramChat' }, {
chatBotLink: (
<SecondaryLink target='_blank' rel='noopener noreferrer' href={HelpRequisites?.support_bot ? `https://t.me/${HelpRequisites.support_bot}` : '#'}>
{ChatInTelegramMessage}
</SecondaryLink>
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx` around lines 57
- 61, The external Telegram link in the InstructionStepSupportTelegramChat JSX
uses target='_blank' without rel attributes; update the SecondaryLink element
(used in InstructionStepSupportTelegramChat) to include rel='noopener
noreferrer' when rendering the href built from HelpRequisites?.support_bot and
ChatInTelegramMessage so the external link is safe from reverse-tabnabbing.

Comment on lines +114 to +184
const startConfirmEmailAction = useCallback(async () => {
if (!userIdRef.current) {
throw new Error('No userid for StartConfirmEmailAction!')
}

const shouldWait = confirmEmailTokenExpiredDate && dayjs(confirmEmailTokenExpiredDate) > dayjs()
if (shouldWait) return

setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())

const sender = getClientSideSenderInfo()
const captcha = await executeCaptcha()

const res = await startConfirmEmailActionMutation({
variables: {
data: {
dv: 1,
sender,
captcha,
user: {
id: userIdRef.current,
},
},
},
})

setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())

const token = res?.data?.result?.token
if (token) {
setConfirmEmailToken(token)
}

return res
}, [executeCaptcha, startConfirmEmailActionMutation, confirmEmailTokenExpiredDate])

const startConfirmPhoneAction = useCallback(async () => {
if (!userIdRef.current) {
throw new Error('No userid for StartConfirmPhoneAction!')
}

const shouldWait = confirmPhoneTokenExpiredDate && dayjs(confirmPhoneTokenExpiredDate) > dayjs()
if (shouldWait) return

setConfirmPhoneTokenExpiredDate(dayjs().add(1, 'minute').toISOString())

const sender = getClientSideSenderInfo()
const captcha = await executeCaptcha()

const res = await startConfirmPhoneActionMutation({
variables: {
data: {
dv: 1,
sender,
captcha,
user: {
id: userIdRef.current,
},
},
},
})

setConfirmPhoneTokenExpiredDate(dayjs().add(1, 'minute').toISOString())

const token = res?.data?.result?.token
if (token) {
setConfirmPhoneToken(token)
}

return res
}, [executeCaptcha, startConfirmPhoneActionMutation, confirmPhoneTokenExpiredDate])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid blocking resend when the startConfirm mutation fails.*

Setting the expiry before the mutation prevents retries for a minute even if the request fails. Move the expiry update to the success path (after token retrieval).

🛠️ Proposed fix
-        setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
-
         const sender = getClientSideSenderInfo()
         const captcha = await executeCaptcha()
@@
-        setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
-
         const token = res?.data?.result?.token
         if (token) {
             setConfirmEmailToken(token)
+            setConfirmEmailTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
         }
@@
-        setConfirmPhoneTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
-
         const sender = getClientSideSenderInfo()
         const captcha = await executeCaptcha()
@@
-        setConfirmPhoneTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
-
         const token = res?.data?.result?.token
         if (token) {
             setConfirmPhoneToken(token)
+            setConfirmPhoneTokenExpiredDate(dayjs().add(1, 'minute').toISOString())
         }
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx` around lines
114 - 184, Both startConfirmEmailAction and startConfirmPhoneAction currently
set the token expiry before performing the mutation, which blocks resends even
when the mutation fails; remove the pre-mutation
setConfirmEmailTokenExpiredDate/setConfirmPhoneTokenExpiredDate calls and
instead set the expiry only on success (after you obtain token from
res?.data?.result?.token, e.g. inside the if (token) block) so failures do not
start the cooldown; keep the initial shouldWait check, captcha execution
(executeCaptcha), and mutations
(startConfirmEmailActionMutation/startConfirmPhoneActionMutation) unchanged
otherwise.

Comment on lines +2454 to +2457
"pages.condo.profile.2FA.title": "Двухфакторная авторизация",
"pages.condo.profile.2FA.tooltip.emptyFactors": "Чтобы мы могли присылать вам код, укажите {factors}. Для этого во вкладке «Информация» нажмите «Редактировать» и добавьте недостающие данные.",
"pages.condo.profile.2FA.tooltip.notVerifiedEmail": "Чтобы мы могли присылать вам код, подтвердите ваш email. Для этого проверьте почту и прочитайте письмо от {companyName}",
"pages.condo.profile.2FA.tooltip.notVerifiedPhone": "Чтобы мы могли присылать вам код, подтвердите ваш номер телефон",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Опечатка: «номер телефон» → «номер телефона».

✏️ Proposed fix
-"pages.condo.profile.2FA.tooltip.notVerifiedPhone": "Чтобы мы могли присылать вам код, подтвердите ваш номер телефон",
+"pages.condo.profile.2FA.tooltip.notVerifiedPhone": "Чтобы мы могли присылать вам код, подтвердите ваш номер телефона",
🤖 Prompt for AI Agents
In `@apps/condo/lang/ru/ru.json` around lines 2454 - 2457, Update the Russian
translation value for the key pages.condo.profile.2FA.tooltip.notVerifiedPhone
to correct the typo: replace "Чтобы мы могли присылать вам код, подтвердите ваш
номер телефон" with "Чтобы мы могли присылать вам код, подтвердите ваш номер
телефона" so the phrase uses the correct genitive form; leave the rest of the
JSON key/value intact.

@Alllex202 Alllex202 force-pushed the feat/condo/DOMA-12564/2fa-frontend branch from 34ee8ba to a0cb6ff Compare February 10, 2026 07:48
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
13.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
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: 12

Caution

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

⚠️ Outside diff range comments (4)
apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.test.js (1)

31-38: ⚠️ Potential issue | 🟡 Minor

Pre-existing: mutation fragment doesn't select token, but line 243 asserts on it.

The AUTHENTICATE_WITH_PHONE_AND_PASSWORD_MUTATION_WITH_TOKEN GQL query only selects item { id } — it never requests token. Line 243 then asserts result.data.result.token is not null, which would always be undefined unless the server returns it implicitly. This isn't part of your change, but worth noting since the test name explicitly says "return token."

apps/condo/domains/user/schema/ConfirmEmailActionService.js (2)

269-283: ⚠️ Potential issue | 🟡 Minor

New error types missing from doc.errors for startConfirmEmailAction.

EMAIL_AND_USER_ID_IS_MISSING and SHOULD_BE_ONE_IDENTIFIER_ONLY are thrown by the resolver (lines 303-308) but not listed in the doc.errors block. They won't appear in GraphiQL documentation.

Proposed fix
             doc: {
                 summary: 'Send confirmation email and return confirmation token...',
                 errors: {
                     ...pick(ERRORS, [
                         DV_VERSION_MISMATCH,
                         'WRONG_SENDER_FORMAT',
                         CAPTCHA_CHECK_FAILED,
                         UNABLE_TO_FIND_CONFIRM_EMAIL_ACTION,
                         'WRONG_EMAIL_FORMAT',
                         'GENERATE_TOKEN_ERROR',
+                        'EMAIL_AND_USER_ID_IS_MISSING',
+                        'SHOULD_BE_ONE_IDENTIFIER_ONLY',
                     ]),
                 },
             },

319-333: ⚠️ Potential issue | 🟡 Minor

When user has no email, the error message is misleading.

If User.getOne finds a user but user.email is null, email becomes null (line 323), and normalizeEmail(null) returns falsy, throwing WRONG_EMAIL_FORMAT. The error says "Wrong format of provided email" — confusing when the real issue is the user has no email on file. Consider adding an explicit check or a dedicated error type for this case.

Additionally, on line 333, checkEmailSendingLimits receives the raw (un-normalized) email. For the emailFromInput path, different casings of the same address could bypass rate limits. Consider using normalizedEmail instead:

+                const normalizedEmail = normalizeEmail(email)
+                if (!normalizedEmail) {
+                    throw new GQLError(ERRORS.WRONG_EMAIL_FORMAT, context)
+                }
 
-                const normalizedEmail = normalizeEmail(email)
-                if (!normalizedEmail) {
-                    throw new GQLError(ERRORS.WRONG_EMAIL_FORMAT, context)
-                }
-
-                await checkEmailSendingLimits(context, email, ip)
+                await checkEmailSendingLimits(context, normalizedEmail, ip)
apps/condo/domains/user/schema/ConfirmPhoneActionService.js (1)

217-231: ⚠️ Potential issue | 🟡 Minor

New error types missing from doc.errors for startConfirmPhoneAction.

Same issue as ConfirmEmailActionService.jsPHONE_AND_USER_ID_IS_MISSING and SHOULD_BE_ONE_IDENTIFIER_ONLY are thrown by the resolver but not listed in doc.errors.

Proposed fix
             doc: {
                 summary: 'Send confirmation phone SMS message...',
                 errors: {
                     ...pick(ERRORS, [
                         CAPTCHA_CHECK_FAILED,
                         WRONG_PHONE_FORMAT,
+                        'PHONE_AND_USER_ID_IS_MISSING',
+                        'SHOULD_BE_ONE_IDENTIFIER_ONLY',
                     ]),
                     TOO_MANY_REQUESTS: GQL_ERRORS.TOO_MANY_REQUESTS,
🤖 Fix all issues with AI agents
In `@apps/condo/domains/common/constants/regexps.js`:
- Line 20: The shared constant NOT_NUMBER_REGEX uses the global flag and is
stateful (lastIndex), so replace the constant with a factory function (e.g.,
getNotNumberRegex or createNotNumberRegex) that returns a fresh RegExp(/\D/g)
each call, and update all call sites to use that factory (or call the factory
only where .test()/.exec() might be used) so no consumer shares a mutated regex
instance; alternatively add a clear comment on NOT_NUMBER_REGEX’s intended use
and change callers that use .test()/.exec() to instantiate a new RegExp(/\D/g)
instead of reusing the constant.

In `@apps/condo/domains/common/utils/helpers.ts`:
- Around line 28-36: The formatPhone function's signature declares a string
return but can return undefined via the final "return phone" path when phone?:
string is undefined (especially when isMaskedPhone is true and normalizePhone is
bypassed); update the function return type from string to string | undefined on
the formatPhone declaration and ensure any downstream usages that assume a
non-null string handle the possible undefined accordingly; locate the
formatPhone function and adjust its signature and any related type expectations
(references: formatPhone, normalizePhone, isMaskedPhone).

In `@apps/condo/domains/user/components/auth/RegisterForm.tsx`:
- Around line 211-231: The SecondFactorHeader useCallback currently lists only
[onReset] but captures SecondFactorForAuthorizationTitle; update the dependency
array for SecondFactorHeader to include SecondFactorForAuthorizationTitle so the
callback re-creates when the title changes (i.e., use [onReset,
SecondFactorForAuthorizationTitle]); locate the useCallback declaration named
SecondFactorHeader in RegisterForm.tsx and add SecondFactorForAuthorizationTitle
to its dependency list.

In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx`:
- Around line 87-95: The empty onError handlers for
useCompleteConfirmEmailActionMutation and useCompleteConfirmPhoneActionMutation
silently swallow network errors; update these mutations
(completeConfirmEmailMutation and completeConfirmPhoneMutation) to handle the
error argument and surface it (e.g., call confirmActionErrorHandler(error) or
log and show a generic notification/toast) instead of using onError: () => {},
and ensure startConfirmPhoneActionMutation continues to use
confirmActionErrorHandler for consistent user feedback.
- Line 268: The Row component's style prop is receiving a boolean false when
currentSecondFactor is truthy because the expression "!currentSecondFactor && {
display: 'none' }" returns false; change this to an explicit ternary so style
always receives either an object or undefined (for example using
"currentSecondFactor ? undefined : { display: 'none' }" or equivalent) to avoid
passing boolean to the style prop on the Row component in SecondFactorForm.

In `@apps/condo/domains/user/components/UserSettingsContent.tsx`:
- Around line 264-267: handle2FAEnabledChange currently awaits change2FA without
error handling and only updates is2FAEnabled on success, causing unhandled
rejections and a stuck UI on failure; wrap the async body of
handle2FAEnabledChange in try/catch, call setIs2FAEnabled(checked)
optimistically before awaiting or keep as-is but on catch reset
setIs2FAEnabled(prevValue) (or set to !checked) to revert the Switch, and
surface the error via your app’s notification API (e.g.,
message.error/toast/error notification) so failures are handled and the UI is
reverted and user-informed; update references to change2FA,
handle2FAEnabledChange, and setIs2FAEnabled accordingly.

In
`@apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js`:
- Around line 149-161: The confirm-action updates (ConfirmEmailAction.update and
ConfirmPhoneAction.update) are applied before calling startAuthedSession, which
can consume one-time tokens if session creation fails; move the completion
updates to after startAuthedSession or wrap both the session creation and the
ConfirmEmailAction/ConfirmPhoneAction.update calls in a single atomic
operation/transaction so the actions are only marked completed when
startAuthedSession succeeds—locate the block around startAuthedSession and the
two Confirm*Action.update calls and either reorder to call startAuthedSession
first and then update the actions, or perform them inside a
transaction/try-commit so failures roll back the action completions.
- Around line 131-147: The branch currently treats NOT_ENOUGH_AUTH_FACTORS as
WRONG_CREDENTIALS when validation._error.is2FAEnabled is false; change the logic
in AuthenticateUserWithPhoneAndPasswordService so that whenever
validation._error.errorType === ERROR_TYPES.NOT_ENOUGH_AUTH_FACTORS you throw a
GQLError reflecting that specific failure instead of falling through to
ERRORS.WRONG_CREDENTIALS — if is2FAEnabled is true keep the existing authDetails
payload, and if false throw a GQLError using a clear error constant/payload
(e.g., a dedicated ERRORS.NOT_ENOUGH_AUTH_FACTORS_NO_2FA or the same
NOT_ENOUGH_AUTH_FACTORS with is2FAEnabled:false) so callers/tests can
distinguish “no auth factors provided” from “wrong credentials.”

In `@apps/condo/pages/auth/change-password.tsx`:
- Around line 554-558: Replace the usage of the children prop on the Button with
JSX children: remove the children prop from the Button element and place the
combined label (the template combining SaveMsg and AndSignInMessage) between the
Button's opening and closing tags; keep the other props (loading={isLoading},
htmlType='submit', block) unchanged so Button renders the same text via its
inner JSX rather than a children prop.
- Around line 393-423: The NOT_ENOUGH_AUTH_FACTORS branch is unreachable because
it checks error.originalErrors.graphQLErrors, but Apollo provides
error.graphQLErrors and originalErrors (when present) is a flat GraphQLError[];
update the detection to inspect both shapes: check
error?.graphQLErrors?.some(isNotEnoughAuthFactors) and, if not present, check
Array.isArray(error?.originalErrors) &&
error.originalErrors.some(isNotEnoughAuthFactors). Use the same
isNotEnoughAuthFactors predicate, then find the matching GraphQLError from
whichever array matched, extract extensions.authDetails and proceed with the
existing logic that sets userIdRef, setUserMaskedData,
setAvailableSecondFactors, setCurrentSecondFactor, and the Router.push/return
path; also keep the form.resetFields(['confirmPhoneCode','confirmEmailCode'])
when step === 'changePassword'. Ensure you remove the incorrect property access
and the `@ts-ignore` usages while handling both possible error shapes.
- Around line 296-330: The error handler must treat NOT_ENOUGH_AUTH_FACTORS
raised by authWithIdentifierAndPassword the same when the user is
re-authenticating after a password change (step may be 'changePassword' due to
stale closure); update the handler to detect NOT_ENOUGH_AUTH_FACTORS and either
check if step === 'getSudoTokenBeforeChangePassword' || step ===
'changePassword' or read a live ref (e.g., stepRef.current) instead of the stale
state, then populate authFactors/authFactorsRef (using currentSecondFactor,
confirmEmailToken, confirmPhoneToken), set available second factors
(setAvailableSecondFactors), and transition into the 2FA flow
(setCurrentSecondFactor / setStep to 'authentication' or the existing 2FA step)
so users are prompted for their second factor instead of being redirected to
sign-in.

In `@apps/condo/schema.graphql`:
- Around line 102203-102208: Update the documented error payloads in
schema.graphql so messageForUser shows the i18n keys returned at runtime (not
the hardcoded English text): replace "Not enough auth factors" with
"api.user.NOT_ENOUGH_AUTH_FACTORS" for the NOT_ENOUGH_AUTH_FACTORS entries used
by the authenticateUserWithEmailAndPassword,
authenticateUserWithPhoneAndPassword, generateSudoToken, and
authenticateOrRegisterUserWithToken mutation docs (match the same messageForUser
values defined in the service constants).
🧹 Nitpick comments (19)
apps/condo/domains/user/schema/AuthenticateUserWithEmailAndPasswordService.test.js (1)

183-209: availableSecondFactors assertion is order-dependent — consider expect.arrayContaining.

Line 205: The exact array ['confirmEmailToken', 'confirmPhoneToken'] will fail if the server returns factors in a different order. Using expect.arrayContaining makes this resilient to ordering changes while still validating the content.

Proposed fix
                     authDetails: expect.objectContaining({
                         is2FAEnabled: true,
                         userId: user.id,
-                        availableSecondFactors: ['confirmEmailToken', 'confirmPhoneToken'],
+                        availableSecondFactors: expect.arrayContaining(['confirmEmailToken', 'confirmPhoneToken']),
                         maskedData: { email: maskNormalizedEmail(userAttrs.email), phone: maskNormalizedPhone(userAttrs.phone) },
                     }),
apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.test.js (1)

148-174: availableSecondFactors assertion relies on exact array order.

Line 170 asserts ['confirmEmailToken', 'confirmPhoneToken'] with strict equality (via objectContaining). If the server ever returns these in a different order, the test will break despite correct behavior. Consider using expect.arrayContaining combined with a length check for a more resilient assertion.

-                    availableSecondFactors: ['confirmEmailToken', 'confirmPhoneToken'],
+                    availableSecondFactors: expect.arrayContaining(['confirmEmailToken', 'confirmPhoneToken']),

(And optionally assert .toHaveLength(2) on the array separately if you want to ensure no extra factors.)

apps/condo/domains/common/utils/phone.spec.js (1)

78-96: Well-structured test cases for the new masking utility.

The coverage is good across varying lengths. Consider adding a case for undefined/null input to document the expected behavior when no phone is provided, similar to how normalizePhone tests handle the "no data" scenario.

apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js (2)

13-15: Mixing relative and absolute @condo/ imports for modules in the same domain.

Lines 6–11 import from @condo/domains/user/… while lines 13–15 import from ../constants/… and ../utils/…. Both resolve to the same domain. Prefer one style consistently — the rest of this file (and the codebase) uses @condo/… aliases.

Proposed fix
-const { AUTH_FACTOR_TYPES } = require('../constants/authFactors')
-const { NOT_ENOUGH_AUTH_FACTORS } = require('../constants/errors')
-const { ConfirmPhoneAction, ConfirmEmailAction } = require('../utils/serverSchema')
+const { AUTH_FACTOR_TYPES } = require('@condo/domains/user/constants/authFactors')
+const { NOT_ENOUGH_AUTH_FACTORS } = require('@condo/domains/user/constants/errors')
+const { ConfirmPhoneAction, ConfirmEmailAction } = require('@condo/domains/user/utils/serverSchema')

122-129: Spreading null is valid but less obvious than spreading an empty object.

...(condition ? { key: val } : null) works because ...null is a no-op, but ...{} communicates the "no properties" intent more clearly and avoids surprising readers.

Proposed fix
                 const validation = await validateUserCredentials(
                     { phone, userType },
                     {
                         password,
-                        ...(secondFactor?.type === AUTH_FACTOR_TYPES.CONFIRM_EMAIL_TOKEN ? { confirmEmailToken: secondFactor?.value || '' } : null),
-                        ...(secondFactor?.type === AUTH_FACTOR_TYPES.CONFIRM_PHONE_TOKEN ? { confirmPhoneToken: secondFactor?.value || '' } : null),
+                        ...(secondFactor?.type === AUTH_FACTOR_TYPES.CONFIRM_EMAIL_TOKEN ? { confirmEmailToken: secondFactor?.value || '' } : {}),
+                        ...(secondFactor?.type === AUTH_FACTOR_TYPES.CONFIRM_PHONE_TOKEN ? { confirmPhoneToken: secondFactor?.value || '' } : {}),
                     }
                 )
apps/condo/domains/user/components/auth/SecondFactorForm.tsx (2)

31-37: Hook parameters lack TypeScript typing.

The hook accepts form, onSubmit, Header, isLoading, and setIsLoading without any type annotations. This loses type safety on a public API that's consumed by SignInForm, RegisterForm, and the change-password page.

Suggested type definition
+type UseSecondFactorParams = {
+    form: FormInstance
+    onSubmit: (args: { ... }) => Promise<void>
+    Header?: React.ComponentType<{ setCurrentSecondFactor: ..., setAvailableSecondFactors: ... }>
+    isLoading: boolean
+    setIsLoading: (v: boolean) => void
+}
+
-export const useSecondFactor = ({
-    form,
-    onSubmit,
-    Header,
-    isLoading,
-    setIsLoading,
-}) => {
+export const useSecondFactor = ({
+    form,
+    onSubmit,
+    Header,
+    isLoading,
+    setIsLoading,
+}: UseSecondFactorParams) => {

254-265: Missing startConfirmPhoneAction and startConfirmEmailAction in useEffect deps.

These callbacks are omitted from the dependency array, likely to avoid re-triggering on every callback recreation. However, this means the effect can capture stale closures (e.g., an outdated shouldWait check). Consider using refs for the callbacks or adding them to the deps if the callbacks are properly memoized.

apps/condo/domains/user/components/SudoTokenProvider.tsx (3)

567-571: Fragile manual string interpolation for i18n descriptions.

ConfirmPhoneTokenDescription.replace('{phone}', ...) and ConfirmEmailTokenDescription.replace('{email}', ...) bypass intl.formatMessage's built-in interpolation. If the translation ever renames the placeholder (e.g. {phoneNumber}), this breaks silently.

Instead, call intl.formatMessage at the point of use with the value:

Proposed fix (phone example)
-                                        <FormItem
-                                            required={false}
-                                            name='confirmPhoneCode'
-                                            label={ConfirmPhoneTokenDescription.replace('{phone}', formatPhone(userMaskedData?.phone || '', true))}
-                                        >
+                                        <FormItem
+                                            required={false}
+                                            name='confirmPhoneCode'
+                                            label={intl.formatMessage(
+                                                { id: 'component.SecondFactorForm.type.confirmPhoneToken.description' },
+                                                { phone: formatPhone(userMaskedData?.phone || '', true) },
+                                            )}
+                                        >

Apply the same pattern for confirmEmailCode.

Also applies to: 592-598


108-108: Misleading state setter name setConfirmActionType for currentSecondFactor.

The destructured setter setConfirmActionType doesn't match the state variable currentSecondFactor, making the code harder to follow. Consider renaming for clarity:

-    const [currentSecondFactor, setConfirmActionType] = useState<'confirmPhoneToken' | 'confirmEmailToken'>(null)
+    const [currentSecondFactor, setCurrentSecondFactor] = useState<'confirmPhoneToken' | 'confirmEmailToken'>(null)

This requires updating all usages of setConfirmActionType throughout the file.


291-293: Early return on res?.errors?.length > 0 masks the 2FA flow.

After the NOT_ENOUGH_AUTH_FACTORS block (lines 265-285), execution falls through to line 291 which checks res?.errors?.length > 0 and returns early. This means even if the 2FA block correctly fires (once the graphQLErrors bug is fixed), the function also exits here. This is likely intentional (the 2FA block sets state and shouldn't resolve the promise), but the flow depends on the 2FA block not having set the token — which it doesn't. Worth a brief comment clarifying the intent.

apps/condo/domains/user/components/auth/SignInForm.tsx (2)

107-117: handleSubmit parameter object is untyped.

The destructured parameter object has no type annotation, defeating TypeScript's type-safety for this callback's contract with the useSecondFactor hook. If the hook changes the shape of the object it passes, this will silently break.

Consider defining a type for the parameter:

Suggested approach
+type HandleSubmitParams = {
+    isLoading: boolean
+    setIsLoading: (v: boolean) => void
+    userIdRef: React.MutableRefObject<string | null>
+    setUserMaskedData: (data: { email?: string, phone?: string } | null) => void
+    setAvailableSecondFactors: (factors: Array<'confirmPhoneToken' | 'confirmEmailToken'>) => void
+    setCurrentSecondFactor: (factor: 'confirmPhoneToken' | 'confirmEmailToken' | null) => void
+    currentSecondFactor: 'confirmPhoneToken' | 'confirmEmailToken' | null
+    confirmEmailToken: string | null
+    confirmPhoneToken: string | null
+}

-    const handleSubmit = useCallback(async ({
-        isLoading,
-        setIsLoading,
-        userIdRef,
-        setUserMaskedData,
-        setAvailableSecondFactors,
-        setCurrentSecondFactor,
-        currentSecondFactor,
-        confirmEmailToken,
-        confirmPhoneToken,
-    }): Promise<void> => {
+    const handleSubmit = useCallback(async ({
+        isLoading,
+        setIsLoading,
+        userIdRef,
+        setUserMaskedData,
+        setAvailableSecondFactors,
+        setCurrentSecondFactor,
+        currentSecondFactor,
+        confirmEmailToken,
+        confirmPhoneToken,
+    }: HandleSubmitParams): Promise<void> => {

Ideally this type should be exported from the useSecondFactor hook or shared module so both sides stay in sync. Based on learnings, prefer using type aliases (type X = { ... }) over interface declarations for object shapes.


269-270: Unusual inline style pattern: style={!!currentSecondFactor && { display: 'none' }}

When currentSecondFactor is falsy, this evaluates to style={false}, which React ignores (it works). However, style={undefined} is the conventional way to express "no style". Consider:

-        <Row style={!!currentSecondFactor && { display: 'none' }}>
+        <Row style={currentSecondFactor ? { display: 'none' } : undefined}>
apps/condo/gql/index.ts (1)

8692-8724: Align codegen output with repo JS/TS formatting rules.

This generated block still uses semicolons and 8‑space indentation. Since this file is auto-generated, consider updating the GraphQL codegen config (or excluding this file from lint) so the output matches repo formatting and avoids future lint churn.

As per coding guidelines: "Use 4 spaces for indentation in JavaScript/TypeScript" and "Never use semicolons".

apps/condo/domains/user/components/auth/RegisterForm.tsx (1)

271-271: Assigning undefined to a typed variable bypasses type safety.

AuthenticateOrRegisterUserWithTokenSecondFactorInput likely expects an object with value and type fields, not undefined. Use null or declare the variable as ... | undefined explicitly to make intent clear.

🔧 Proposed fix
-            let secondFactor: AuthenticateOrRegisterUserWithTokenSecondFactorInput = undefined
+            let secondFactor: AuthenticateOrRegisterUserWithTokenSecondFactorInput | undefined = undefined
apps/condo/domains/user/components/UserSettingsContent.tsx (1)

70-90: EmptyFactorsTooltipMessage logic may show misleading messages.

When user.email exists but isEmailVerified is false, the tooltip says the email is not verified. But it doesn't check whether the phone is also unverified — if both are unverified, only the email message is shown, potentially hiding the phone verification issue. Consider whether both conditions should be surfaced.

apps/condo/pages/auth/change-password.tsx (1)

530-530: style prop receives false when condition is falsy.

(currentSecondFactor || step !== 'changePassword') && { display: 'none' } evaluates to false when the left side is falsy. While React ignores false for style, this is type-unsafe and may confuse TS/readers. Consider a ternary or undefined.

🔧 Proposed fix
-                <Row justify='center' style={(currentSecondFactor || step !== 'changePassword') && { display: 'none' }}>
+                <Row justify='center' style={(currentSecondFactor || step !== 'changePassword') ? { display: 'none' } : undefined}>
apps/condo/domains/user/schema/AuthenticateOrRegisterUserWithTokenService.js (2)

454-466: authDetails exposure in error extensions is intentional but worth noting.

The error response leaks userId, availableSecondFactors, and maskedData to the client. This is necessary for the frontend 2FA flow but means anyone with a valid first factor (confirmed phone/email token) can enumerate user IDs and available 2FA methods. Since the first factor already requires CAPTCHA + token verification, the risk is limited — just flagging for awareness.


273-280: GraphQL enum uses camelCase values — unconventional but functional.

The enum AuthenticateOrRegisterUserWithTokenSecondFactorType generates values like confirmPhoneToken, confirmEmailToken, password (camelCase). GraphQL convention is UPPER_CASE for enum values (e.g., CONFIRM_PHONE_TOKEN). This works as long as the generated TypeScript types align, but it may surprise developers familiar with GraphQL conventions.

apps/condo/schema.graphql (1)

90057-90060: Consider consolidating the three near-identical second-factor enum types.

AuthenticateUserWithPhoneAndPasswordSecondFactorType, AuthenticateOrRegisterUserWithTokenSecondFactorType, and AuthenticateUserWithEmailAndPasswordSecondFactorType share confirmEmailToken and confirmPhoneToken. Only the token-based variant adds password. A single shared enum (e.g., SecondFactorType) would reduce duplication, though separate types do allow independent evolution per auth flow.

Also applies to: 90282-90286, 90361-90364

const URL_REGEX = new RegExp('(https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})', 'gm')
const URL_WITH_CYRILLIC_REGEX = new RegExp('(https?:\\/\\/(?:www\\.|(?!www))[a-zа-я0-9][a-zа-я0-9-]+[a-zа-я0-9]\\.[^\\s]{2,}|www\\.[a-zа-я0-9][a-zа-я0-9-]+[a-zа-я0-9]\\.[^\\s]{2,}|https?:\\/\\/(?:www\\.|(?!www))[a-zа-я0-9]+\\.[^\\s]{2,}|www\\.[a-zа-я0-9]+\\.[^\\s]{2,})', 'gmi')
const IPv4_REGEX = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
const NOT_NUMBER_REGEX = /\D/g
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Shared regex with global flag is stateful — safe for .replace() but risky for .test()/.exec().

/\D/g carries mutable lastIndex state. Current usage via String.prototype.replace is fine because replace resets lastIndex internally. However, if a future consumer calls .test() or .exec() on this shared constant, it will produce intermittent incorrect results due to lastIndex not resetting between calls.

Consider documenting the intended usage or dropping the g flag if it's only consumed by .replace() (which applies globally by default when the pattern is a regex with g). Since .replace() without g only replaces the first match, the g flag is actually required here — just be aware of the pitfall for other callers.

🤖 Prompt for AI Agents
In `@apps/condo/domains/common/constants/regexps.js` at line 20, The shared
constant NOT_NUMBER_REGEX uses the global flag and is stateful (lastIndex), so
replace the constant with a factory function (e.g., getNotNumberRegex or
createNotNumberRegex) that returns a fresh RegExp(/\D/g) each call, and update
all call sites to use that factory (or call the factory only where
.test()/.exec() might be used) so no consumer shares a mutated regex instance;
alternatively add a clear comment on NOT_NUMBER_REGEX’s intended use and change
callers that use .test()/.exec() to instantiate a new RegExp(/\D/g) instead of
reusing the constant.

Comment on lines +28 to 36
export const formatPhone = (phone?: string, isMaskedPhone: boolean = false): string => {
const normalizedPhone = isMaskedPhone ? phone : normalizePhone(phone, true)
if (normalizedPhone?.startsWith('+7')){
return normalizedPhone.replace(RUSSIAN_PHONE_FORMAT_REGEXP, '$1 ($2) $3-$4-$5')
return normalizedPhone.replace(isMaskedPhone ? RUSSIAN_MASKED_PHONE_FORMAT_REGEXP : RUSSIAN_PHONE_FORMAT_REGEXP, '$1 ($2) $3-$4-$5')
}
if (normalizedPhone?.startsWith('+34')){
return normalizedPhone.replace(SPANISH_PHONE_FORMAT_REGEXP, '$1-$2-$3-$4')
return normalizedPhone.replace(isMaskedPhone ? SPANISH_MASKED_PHONE_FORMAT_REGEXP : SPANISH_PHONE_FORMAT_REGEXP, '$1-$2-$3-$4')
}
return phone
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/condo/domains/common/utils/helpers.ts | head -60

Repository: open-condo-software/condo

Length of output: 2601


🏁 Script executed:

rg -n 'formatPhone\s*\(' --type=ts --type=tsx -g '!*.spec.*' -g '!*.test.*' -A2 -B2

Repository: open-condo-software/condo

Length of output: 96


🏁 Script executed:

rg -n 'formatPhone\s*\(' -A2 -B2 --type-not=test --type-not=spec -g '!*.spec.ts' -g '!*.test.ts'

Repository: open-condo-software/condo

Length of output: 97


🏁 Script executed:

rg -n 'formatPhone\s*\(' -A2 -B2 -g '!*.spec.ts' -g '!*.test.ts' -g '!*.spec.js' -g '!*.test.js'

Repository: open-condo-software/condo

Length of output: 7142


🏁 Script executed:

rg -n 'userMaskedData' -B5 -A5 'SecondFactorForm.tsx'

Repository: open-condo-software/condo

Length of output: 129


🏁 Script executed:

fd -t f 'SecondFactorForm.tsx'

Repository: open-condo-software/condo

Length of output: 129


🏁 Script executed:

rg -n 'userMaskedData' apps/condo/domains/user/components/auth/SecondFactorForm.tsx -B5 -A5

Repository: open-condo-software/condo

Length of output: 2347


🏁 Script executed:

rg -n 'setUserMaskedData' apps/condo/domains/user/components/auth/SecondFactorForm.tsx -B10 -A10

Repository: open-condo-software/condo

Length of output: 2213


🏁 Script executed:

rg -n 'setUserMaskedData\(' apps/condo/domains/user/components/auth/SecondFactorForm.tsx -A10 | head -40

Repository: open-condo-software/condo

Length of output: 51


🏁 Script executed:

rg -n 'const onSubmit' apps/condo/domains/user/components/auth/SecondFactorForm.tsx -B5 -A30 | head -60

Repository: open-condo-software/condo

Length of output: 1464


🏁 Script executed:

rg -n 'interface.*SecondFactorFormProps\|type.*SecondFactorFormProps' apps/condo/domains/user/components/auth/SecondFactorForm.tsx -A20

Repository: open-condo-software/condo

Length of output: 51


🏁 Script executed:

head -50 apps/condo/domains/user/components/auth/SecondFactorForm.tsx

Repository: open-condo-software/condo

Length of output: 2942


🏁 Script executed:

fd -t f 'SecondFactorForm.ts' apps/condo/domains/user

Repository: open-condo-software/condo

Length of output: 129


🏁 Script executed:

rg -n 'setUserMaskedData.*phone' apps/condo/domains/user -B5 -A5

Repository: open-condo-software/condo

Length of output: 2698


🏁 Script executed:

rg -n 'setUserMaskedData\(\{' apps/condo/domains/user -B10 -A5

Repository: open-condo-software/condo

Length of output: 51


🏁 Script executed:

rg -n 'setUserMaskedData' apps/condo/domains/user -A2 -B2

Repository: open-condo-software/condo

Length of output: 4491


Fix return type annotation to reflect that undefined can be returned.

The function signature declares string as the return type, but line 36 returns phone which can be undefined. When isMaskedPhone is true, the input bypasses normalizePhone, so any phone string that doesn't start with +7 or +34 falls through to return phone and may return undefined if the input was undefined. Update the return type to string | undefined to accurately reflect this behavior.

The concern about whether callers pass pre-normalized masked phones when isMaskedPhone=true is valid—callers found in SecondFactorForm.tsx and SudoTokenProvider.tsx do pass data from authDetails?.maskedData which is already masked/normalized, mitigating the risk in practice.

🤖 Prompt for AI Agents
In `@apps/condo/domains/common/utils/helpers.ts` around lines 28 - 36, The
formatPhone function's signature declares a string return but can return
undefined via the final "return phone" path when phone?: string is undefined
(especially when isMaskedPhone is true and normalizePhone is bypassed); update
the function return type from string to string | undefined on the formatPhone
declaration and ensure any downstream usages that assume a non-null string
handle the possible undefined accordingly; locate the formatPhone function and
adjust its signature and any related type expectations (references: formatPhone,
normalizePhone, isMaskedPhone).

Comment on lines +211 to +231
const SecondFactorHeader = useCallback(({ setCurrentSecondFactor, setAvailableSecondFactors }) => (
<Col span={24}>
<Row gutter={[0, 16]}>
<Col span={24}>
<Space direction='vertical' size={24}>
<Button.Icon
onClick={() => {
setCurrentSecondFactor(null)
setAvailableSecondFactors([])
onReset()
}}
size='small'
>
<ArrowLeft />
</Button.Icon>
<Typography.Title level={2}>{SecondFactorForAuthorizationTitle}</Typography.Title>
</Space>
</Col>
</Row>
</Col>
), [onReset])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SecondFactorForAuthorizationTitle is missing from useCallback dependency array.

SecondFactorHeader captures SecondFactorForAuthorizationTitle (used at line 226) but the dependency array at line 231 only lists [onReset]. If the intl locale changes, the title won't update.

🔧 Proposed fix
-    }, [onReset])
+    }, [onReset, SecondFactorForAuthorizationTitle])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const SecondFactorHeader = useCallback(({ setCurrentSecondFactor, setAvailableSecondFactors }) => (
<Col span={24}>
<Row gutter={[0, 16]}>
<Col span={24}>
<Space direction='vertical' size={24}>
<Button.Icon
onClick={() => {
setCurrentSecondFactor(null)
setAvailableSecondFactors([])
onReset()
}}
size='small'
>
<ArrowLeft />
</Button.Icon>
<Typography.Title level={2}>{SecondFactorForAuthorizationTitle}</Typography.Title>
</Space>
</Col>
</Row>
</Col>
), [onReset])
const SecondFactorHeader = useCallback(({ setCurrentSecondFactor, setAvailableSecondFactors }) => (
<Col span={24}>
<Row gutter={[0, 16]}>
<Col span={24}>
<Space direction='vertical' size={24}>
<Button.Icon
onClick={() => {
setCurrentSecondFactor(null)
setAvailableSecondFactors([])
onReset()
}}
size='small'
>
<ArrowLeft />
</Button.Icon>
<Typography.Title level={2}>{SecondFactorForAuthorizationTitle}</Typography.Title>
</Space>
</Col>
</Row>
</Col>
), [onReset, SecondFactorForAuthorizationTitle])
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/auth/RegisterForm.tsx` around lines 211 -
231, The SecondFactorHeader useCallback currently lists only [onReset] but
captures SecondFactorForAuthorizationTitle; update the dependency array for
SecondFactorHeader to include SecondFactorForAuthorizationTitle so the callback
re-creates when the title changes (i.e., use [onReset,
SecondFactorForAuthorizationTitle]); locate the useCallback declaration named
SecondFactorHeader in RegisterForm.tsx and add SecondFactorForAuthorizationTitle
to its dependency list.

Comment on lines +87 to +95
const [completeConfirmEmailMutation] = useCompleteConfirmEmailActionMutation({
onError: () => {},
})
const [startConfirmPhoneActionMutation] = useStartConfirmPhoneActionMutation({
onError: confirmActionErrorHandler,
})
const [completeConfirmPhoneMutation] = useCompleteConfirmPhoneActionMutation({
onError: () => {},
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent onError swallows network errors with no user feedback.

The onError: () => {} handlers prevent Apollo from throwing, but network-level errors (as opposed to GraphQL errors in the response) won't appear in res.errors either — they're silently discarded. The user sees no feedback on network failure. Consider at minimum logging or showing a generic error notification.

🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx` around lines 87
- 95, The empty onError handlers for useCompleteConfirmEmailActionMutation and
useCompleteConfirmPhoneActionMutation silently swallow network errors; update
these mutations (completeConfirmEmailMutation and completeConfirmPhoneMutation)
to handle the error argument and surface it (e.g., call
confirmActionErrorHandler(error) or log and show a generic notification/toast)
instead of using onError: () => {}, and ensure startConfirmPhoneActionMutation
continues to use confirmActionErrorHandler for consistent user feedback.

}, [currentSecondFactor])

const SecondFactorForm = useMemo(() => (
<Row justify='center' style={!currentSecondFactor && { display: 'none' }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

style prop receives false when currentSecondFactor is set.

!currentSecondFactor && { display: 'none' } evaluates to false (not undefined) when the factor is set. Use a ternary to be explicit:

-        <Row justify='center' style={!currentSecondFactor && { display: 'none' }}>
+        <Row justify='center' style={!currentSecondFactor ? { display: 'none' } : undefined}>
🤖 Prompt for AI Agents
In `@apps/condo/domains/user/components/auth/SecondFactorForm.tsx` at line 268,
The Row component's style prop is receiving a boolean false when
currentSecondFactor is truthy because the expression "!currentSecondFactor && {
display: 'none' }" returns false; change this to an explicit ternary so style
always receives either an object or undefined (for example using
"currentSecondFactor ? undefined : { display: 'none' }" or equivalent) to avoid
passing boolean to the style prop on the Row component in SecondFactorForm.

Comment on lines +149 to +161
if (validation.confirmEmailAction) {
await ConfirmEmailAction.update(context, validation.confirmEmailAction.id, {
dv, sender,
completedAt: new Date().toISOString(),
})
}

if (validation.confirmPhoneAction) {
await ConfirmPhoneAction.update(context, validation.confirmPhoneAction.id, {
dv, sender,
completedAt: new Date().toISOString(),
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Confirm-action completion runs before the session is established.

If startAuthedSession (line 164) throws, the ConfirmEmailAction/ConfirmPhoneAction records are already marked completedAt, consuming the one-time tokens. The user would need to re-initiate the 2FA flow. Consider wrapping the session start and the action updates together, or marking actions complete after the session is created, so a transient session failure doesn't burn the tokens.

🤖 Prompt for AI Agents
In
`@apps/condo/domains/user/schema/AuthenticateUserWithPhoneAndPasswordService.js`
around lines 149 - 161, The confirm-action updates (ConfirmEmailAction.update
and ConfirmPhoneAction.update) are applied before calling startAuthedSession,
which can consume one-time tokens if session creation fails; move the completion
updates to after startAuthedSession or wrap both the session creation and the
ConfirmEmailAction/ConfirmPhoneAction.update calls in a single atomic
operation/transaction so the actions are only marked completed when
startAuthedSession succeeds—locate the block around startAuthedSession and the
two Confirm*Action.update calls and either reorder to call startAuthedSession
first and then update the actions, or perform them inside a
transaction/try-commit so failures roll back the action completions.

Comment on lines +296 to +330
if (step === 'getSudoTokenBeforeChangePassword') {
const tokenTypeInfo = detectTokenTypeSafely(token)

const status = res?.data?.result?.status
if (!res.errors && status === 'ok') {
invalidateSudoToken()
if (tokenTypeInfo.error) {
throw tokenTypeInfo.error
}
if (!tokenTypeInfo.tokenType) {
throw new Error('Unknown token type')
}
if (!allowedTokenTypes.includes(tokenTypeInfo.tokenType)) {
throw new Error('Unsupported token type')
}

if (!identifier || !identifierType) {
await Router.push(`/auth/signin?next=${redirectUrl}`)
const authFactors: { confirmEmailToken?: string, confirmPhoneToken?: string } = {}
if (tokenTypeInfo.tokenType === TOKEN_TYPES.CONFIRM_EMAIL) {
authFactors.confirmEmailToken = token
}
if (tokenTypeInfo.tokenType === TOKEN_TYPES.CONFIRM_PHONE) {
authFactors.confirmPhoneToken = token
}
if (currentSecondFactor === 'confirmPhoneToken' && tokenTypeInfo.tokenType !== TOKEN_TYPES.CONFIRM_PHONE) {
authFactors.confirmPhoneToken = confirmPhoneToken
}
if (currentSecondFactor === 'confirmEmailToken' && tokenTypeInfo.tokenType !== TOKEN_TYPES.CONFIRM_EMAIL) {
authFactors.confirmEmailToken = confirmEmailToken
}

authFactorsRef.current = authFactors

await generateSudoToken()

setCurrentSecondFactor(null)
setAvailableSecondFactors([])
setStep('changePassword')
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Get file size and relevant lines from the change-password.tsx file
wc -l apps/condo/pages/auth/change-password.tsx

Repository: open-condo-software/condo

Length of output: 114


🏁 Script executed:

# View the handleSubmit function and surrounding context
sed -n '280,420p' apps/condo/pages/auth/change-password.tsx

Repository: open-condo-software/condo

Length of output: 6368


🏁 Script executed:

# Search for where handleSubmit is called and how form submission works
rg -n 'handleSubmit|onFinish' apps/condo/pages/auth/change-password.tsx -B2 -A2

Repository: open-condo-software/condo

Length of output: 613


🏁 Script executed:

# Look for error handling blocks and NOT_ENOUGH_AUTH_FACTORS
rg -n 'NOT_ENOUGH_AUTH_FACTORS|catch|error' apps/condo/pages/auth/change-password.tsx -B2 -A2

Repository: open-condo-software/condo

Length of output: 4628


Missing 2FA handling in error flow during authentication after password change.

When authWithIdentifierAndPassword throws NOT_ENOUGH_AUTH_FACTORS after setStep('authentication') at line 344, the error handler checks if (step === 'getSudoTokenBeforeChangePassword') but step retains the value 'changePassword' from the closure (state updates are asynchronous). The code then falls through to the else branch and redirects to sign-in, bypassing the 2FA challenge. This means users changing password with 2FA enabled will be incorrectly redirected instead of prompted for their second factor during the re-authentication flow.

Add handling for when NOT_ENOUGH_AUTH_FACTORS occurs during the 'changePassword' step—either by checking step === 'changePassword' in the error handler's 2FA condition, or by restructuring to avoid relying on the stale step value in error closures.

🤖 Prompt for AI Agents
In `@apps/condo/pages/auth/change-password.tsx` around lines 296 - 330, The error
handler must treat NOT_ENOUGH_AUTH_FACTORS raised by
authWithIdentifierAndPassword the same when the user is re-authenticating after
a password change (step may be 'changePassword' due to stale closure); update
the handler to detect NOT_ENOUGH_AUTH_FACTORS and either check if step ===
'getSudoTokenBeforeChangePassword' || step === 'changePassword' or read a live
ref (e.g., stepRef.current) instead of the stale state, then populate
authFactors/authFactorsRef (using currentSecondFactor, confirmEmailToken,
confirmPhoneToken), set available second factors (setAvailableSecondFactors),
and transition into the 2FA flow (setCurrentSecondFactor / setStep to
'authentication' or the existing 2FA step) so users are prompted for their
second factor instead of being redirected to sign-in.

Comment on lines +393 to +423
if (error?.originalErrors) {
const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
// @ts-ignore
if (error?.originalErrors?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
// @ts-ignore
const graphQLError = error?.originalErrors?.graphQLErrors.find(isNotEnoughAuthFactors)
const authDetails = graphQLError?.extensions?.authDetails

if (authDetails?.is2FAEnabled && authDetails?.userId && authDetails?.availableSecondFactors?.length > 0) {
if (step === 'getSudoTokenBeforeChangePassword') {
userIdRef.current = authDetails.userId
const availableSecondFactors = authDetails?.availableSecondFactors?.filter(factor => ['confirmPhoneToken', 'confirmEmailToken'].includes(factor)) || []
const prioritySecondFactor = currentSecondFactor || availableSecondFactors?.[0] || null

if (availableSecondFactors.length > 0) {
setUserMaskedData(authDetails?.maskedData || null)
setAvailableSecondFactors(availableSecondFactors)
setCurrentSecondFactor(prioritySecondFactor)
}
} else {
await Router.push(`/auth/signin?next=${redirectUrl}`)
return
}
}

if (step === 'changePassword') {
form.resetFields(['confirmPhoneCode', 'confirmEmailCode'])
}

return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

NOT_ENOUGH_AUTH_FACTORS detection in the catch block is unreachable.

Two compounding issues make the 2FA routing dead code:

  1. Wrong error shape: For authentication errors, Apollo throws an ApolloError (which has .graphQLErrors), not a custom error with .originalErrors. The error?.originalErrors check at line 393 will be undefined for ApolloError instances.

  2. Wrong property path: Even for the custom errors created at line 199 where originalErrors = res.errors, the value is a flat GraphQLError[] array — not an object with a .graphQLErrors sub-property. So error?.originalErrors?.graphQLErrors?.some(...) at line 396 will always be undefined.

The fix should check error.graphQLErrors (the standard Apollo error property) directly:

🐛 Proposed fix
         } catch (error) {
             console.error('Change password failed')
             console.error(error)

-            if (error?.originalErrors) {
-                const isNotEnoughAuthFactors = (error: GraphQLFormattedError) => error?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
-                // `@ts-ignore`
-                if (error?.originalErrors?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
-                    // `@ts-ignore`
-                    const graphQLError = error?.originalErrors?.graphQLErrors.find(isNotEnoughAuthFactors)
+            const isNotEnoughAuthFactors = (gqlErr: GraphQLFormattedError) => gqlErr?.extensions?.type === NOT_ENOUGH_AUTH_FACTORS
+            if (error?.graphQLErrors?.some(isNotEnoughAuthFactors)) {
+                    const graphQLError = error.graphQLErrors.find(isNotEnoughAuthFactors)
                     const authDetails = graphQLError?.extensions?.authDetails
🤖 Prompt for AI Agents
In `@apps/condo/pages/auth/change-password.tsx` around lines 393 - 423, The
NOT_ENOUGH_AUTH_FACTORS branch is unreachable because it checks
error.originalErrors.graphQLErrors, but Apollo provides error.graphQLErrors and
originalErrors (when present) is a flat GraphQLError[]; update the detection to
inspect both shapes: check error?.graphQLErrors?.some(isNotEnoughAuthFactors)
and, if not present, check Array.isArray(error?.originalErrors) &&
error.originalErrors.some(isNotEnoughAuthFactors). Use the same
isNotEnoughAuthFactors predicate, then find the matching GraphQLError from
whichever array matched, extract extensions.authDetails and proceed with the
existing logic that sets userIdRef, setUserMaskedData,
setAvailableSecondFactors, setCurrentSecondFactor, and the Router.push/return
path; also keep the form.resetFields(['confirmPhoneCode','confirmEmailCode'])
when step === 'changePassword'. Ensure you remove the incorrect property access
and the `@ts-ignore` usages while handling both possible error shapes.

Comment on lines +554 to +558
loading={isLoading}
htmlType='submit'
children={`${SaveMsg} ${AndSignInMessage}`}
block
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid passing children using a prop.

Biome (lint/correctness/noChildrenProp) flags this. The canonical way to pass children in React is via JSX nesting.

🔧 Proposed fix
                             <Button
                                 key='submit'
                                 type='primary'
                                 loading={isLoading}
                                 htmlType='submit'
-                                children={`${SaveMsg} ${AndSignInMessage}`}
                                 block
-                            />
+                            >
+                                {`${SaveMsg} ${AndSignInMessage}`}
+                            </Button>
🧰 Tools
🪛 Biome (2.3.13)

[error] 556-556: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🤖 Prompt for AI Agents
In `@apps/condo/pages/auth/change-password.tsx` around lines 554 - 558, Replace
the usage of the children prop on the Button with JSX children: remove the
children prop from the Button element and place the combined label (the template
combining SaveMsg and AndSignInMessage) between the Button's opening and closing
tags; keep the other props (loading={isLoading}, htmlType='submit', block)
unchanged so Button renders the same text via its inner JSX rather than a
children prop.

Comment on lines +102203 to +102208
`{
"code": "BAD_USER_INPUT",
"type": "NOT_ENOUGH_AUTH_FACTORS",
"message": "Not enough auth factors",
"messageForUser": "Not enough auth factors"
}`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the schema.graphql file at the specified lines
wc -l apps/condo/schema.graphql

Repository: open-condo-software/condo

Length of output: 101


🏁 Script executed:

# Get the context around lines 102203-102208 and 102627-102633
sed -n '102200,102210p' apps/condo/schema.graphql

Repository: open-condo-software/condo

Length of output: 472


🏁 Script executed:

# Check the other location mentioned
sed -n '102624,102636p' apps/condo/schema.graphql

Repository: open-condo-software/condo

Length of output: 413


🏁 Script executed:

# Search for how NOT_ENOUGH_AUTH_FACTORS errors are actually constructed
rg 'NOT_ENOUGH_AUTH_FACTORS' --type ts --type js -C 5

Repository: open-condo-software/condo

Length of output: 50381


Update schema.graphql documentation to reflect actual messageForUser values returned at runtime.

The schema.graphql documentation shows "messageForUser": "Not enough auth factors" (hardcoded English string), but the actual server responses use the i18n key "messageForUser": "api.user.NOT_ENOUGH_AUTH_FACTORS". Update the documented error payloads in the schema to show the i18n key instead of the misleading hardcoded English string. This pattern applies to all four mutations (lines 102203–102208, 102627–102633, and others).

Implementation context

The error definitions in the service files use:

NOT_ENOUGH_AUTH_FACTORS: {
    mutation: 'authenticateUserWithEmailAndPassword',
    code: BAD_USER_INPUT,
    type: NOT_ENOUGH_AUTH_FACTORS,
    message: 'Not enough auth factors',
    messageForUser: 'api.user.NOT_ENOUGH_AUTH_FACTORS',  // i18n key, not hardcoded string
},

This pattern is consistent across AuthenticateUserWithEmailAndPasswordService.js, AuthenticateUserWithPhoneAndPasswordService.js, GenerateSudoTokenService.js, and AuthenticateOrRegisterUserWithTokenService.js.

🤖 Prompt for AI Agents
In `@apps/condo/schema.graphql` around lines 102203 - 102208, Update the
documented error payloads in schema.graphql so messageForUser shows the i18n
keys returned at runtime (not the hardcoded English text): replace "Not enough
auth factors" with "api.user.NOT_ENOUGH_AUTH_FACTORS" for the
NOT_ENOUGH_AUTH_FACTORS entries used by the
authenticateUserWithEmailAndPassword, authenticateUserWithPhoneAndPassword,
generateSudoToken, and authenticateOrRegisterUserWithToken mutation docs (match
the same messageForUser values defined in the service constants).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐘 BIG No so easy to review changes up to 1300 lines of code ✋🙂 Review please Comments are resolved, take a look, please

Development

Successfully merging this pull request may close these issues.

3 participants