Skip to content

Fix onboarding#169

Merged
magicmarc merged 14 commits intomainfrom
fix-onboarding
Dec 24, 2025
Merged

Fix onboarding#169
magicmarc merged 14 commits intomainfrom
fix-onboarding

Conversation

@Benbenzhouz
Copy link
Contributor

Fix onboarding!

Ben Zhou and others added 14 commits December 20, 2025 15:13
- Fix unsafe return type in twilio.module.ts
- Remove unnecessary condition checks in auth.controller.ts
- Fix type definitions in google.strategy.ts
- Add eslint-disable comments for console statements
- Fix type assertions in auth.controller.ts
- Use 'as unknown as Twilio.Twilio' for mock client type assertion
- Add eslint-disable comment for unsafe return type
- Fixes TS2740 error for missing properties in mock object
- Add return types to mock Twilio client functions
- Replace || with ?? operators in auth.controller.ts
- Fix unnecessary optional chain in google.strategy.ts
- Add eslint-disable for console.warn in google.strategy.ts
- Use non-null assertion for validated profile fields
- Fix unnecessary optional chain error
- Add non-null assertion since emails[0] is validated in the check above
- Replace profile?.emails?.[0] with explicit checks
- profile parameter is not optional, so optional chain is unnecessary
- Fixes @typescript-eslint/no-unnecessary-condition error
…eter

- profile is a required parameter, so !profile check is unnecessary
- Use profile.emails.length === 0 instead of !profile.emails[0]
- Destructure name and emails directly from profile after validation
- Fixes @typescript-eslint/no-unnecessary-condition error
- Add apiVersion to Stripe initialization for consistency
- Fix street address parsing for 3-part and 4-part address formats
- Add explicit demo step ID check (step 6) for skip action
- Improve address parsing error message to show missing fields
- Create unified app-config.ts utility for frontend URL and CORS origin
- Support multiple environment variable names for flexibility:
  - APP_URL, FRONTEND_URL, NEXT_PUBLIC_APP_URL (for frontend URL)
  - CORS_ORIGIN, FRONTEND_URL, APP_URL (for CORS origin)
- Update all files to use centralized config functions
- Improve compatibility with different environment variable naming conventions

Files updated:
- src/utils/app-config.ts (new)
- src/main.ts
- src/modules/auth/auth.controller.ts
- src/modules/stripe/stripe.service.ts
- src/modules/google-calendar/calendar-oauth.controller.ts
- Use only APP_URL for frontend URL (remove FRONTEND_URL, NEXT_PUBLIC_APP_URL)
- Use only CORS_ORIGIN for CORS (remove FRONTEND_URL, APP_URL fallbacks)
- Simplify code by using single environment variable names
- Add import statement for getAppUrl from @/utils/app-config
- Fixes TypeScript error: Cannot find name 'getAppUrl'
- Add forgot-password endpoint
- Add reset-password endpoint
- Add ResetPasswordDto
- Update auth service with password reset logic
- Replace getAppUrl() with process.env.APP_URL ?? 'http://localhost:3000'
- Replace getCorsOrigin() with process.env.CORS_ORIGIN
- Ensure environment variable names match main branch exactly
Copilot AI review requested due to automatic review settings December 22, 2025 00:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes issues with the onboarding flow by improving address parsing flexibility, adding better error handling for OAuth flows, and enabling the application to run in development mode without third-party service credentials.

Key changes:

  • Enhanced Australian address parsing with multiple regex patterns and fallback logic to handle various address formats
  • Added validation and error handling for Google OAuth authentication failures
  • Introduced mock clients for Twilio and Google OAuth to allow development without credentials
  • Improved error messages and code formatting consistency

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/utils/app-config.ts Adds centralized utility functions for app URL and CORS origin configuration
src/modules/onboarding/onboarding.service.ts Implements flexible address parsing with multiple regex patterns, manual parsing fallback, and demo step handling
src/modules/health/health.controller.ts Reformats API decorator parameters for improved readability
src/modules/google-calendar/calendar-oauth.controller.ts Adds explicit type annotation for frontend URL variable
src/modules/company/schema/company.schema.ts Removes trailing whitespace
src/modules/auth/strategies/google.strategy.ts Adds validation for Google profile data and mock credentials for development
src/modules/auth/dto/reset-password.dto.ts Removes trailing whitespace
src/modules/auth/auth.service.ts Reorders import statement
src/modules/auth/auth.controller.ts Adds validation checks for OAuth callback and improves type safety in user object construction
src/lib/twilio/twilio.module.ts Introduces mock Twilio client for development without credentials
Comments suppressed due to low confidence (2)

src/modules/auth/auth.controller.ts:225

  • The pattern process.env.APP_URL ?? 'http://localhost:3000' is duplicated three times in this method (lines 178, 191, and 225). This should be extracted to a single variable at the beginning of the method to avoid repetition and ensure consistency.
  googleAuthRedirect(@Req() req: Request, @Res() res: Response): void {
    // Check if user is authenticated
    if (!req.user) {
      const frontendUrl = process.env.APP_URL ?? 'http://localhost:3000';
      res.redirect(`${frontendUrl}/login?error=oauth_failed`);
      return;
    }

    const { user, token, csrfToken } = req.user as {
      user: Record<string, unknown>;
      token: string;
      csrfToken: string;
    };

    // Validate required fields
    if (!token || !csrfToken) {
      const frontendUrl = process.env.APP_URL ?? 'http://localhost:3000';
      res.redirect(`${frontendUrl}/login?error=oauth_incomplete`);
      return;
    }

    // Manually construct safe user object to preserve ObjectId
    const safeUser = {
      _id: user._id?.toString() ?? (user._id as string | undefined) ?? '',
      email: (user.email as string | undefined) ?? '',
      firstName: (user.firstName as string | undefined) ?? '',
      lastName: (user.lastName as string | undefined) ?? '',
      role: (user.role as EUserRole | undefined) ?? EUserRole.user,
      status: (user.status as UserStatus | undefined) ?? UserStatus.active,
    };

    // Set JWT token as httpOnly cookie
    res.cookie('jwtToken', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
      path: '/',
    });

    // Set CSRF token as httpOnly cookie for double submit pattern
    res.cookie('csrfToken', csrfToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
      path: '/',
    });

    // Redirect to frontend with user data (CSRF token is in regular cookie)
    const frontendUrl = process.env.APP_URL ?? 'http://localhost:3000';

src/modules/onboarding/onboarding.service.ts:120

  • The complex address parsing logic introduced in this PR lacks test coverage. Given the multiple regex patterns, fallback parsing logic, and various edge cases being handled (3-part vs 4-part addresses, state/postcode together vs separated), this functionality should have comprehensive unit tests to ensure all parsing paths work correctly and edge cases are handled properly.
    'user.address.full': function (answer, update) {
      const trimmedAnswer = answer.trim();

      // Try multiple regex patterns
      let match = this.AU_ADDR_REGEX_STRICT.exec(trimmedAnswer);
      if (!match?.groups) {
        match = this.AU_ADDR_REGEX_FLEXIBLE.exec(trimmedAnswer);
      }
      if (!match?.groups) {
        match = this.AU_ADDR_REGEX_SEPARATED.exec(trimmedAnswer);
      }

      // If regex doesn't match, try to parse manually from comma-separated values
      if (!match?.groups) {
        const parts = trimmedAnswer
          .split(',')
          .map(p => p.trim())
          .filter(p => p);

        if (parts.length >= 3) {
          // Try to extract state and postcode from the last part
          const lastPart = parts[parts.length - 1];
          const statePostcodeMatch = /^([A-Z]{2,3})\s*(\d{4})$/.exec(lastPart);

          if (statePostcodeMatch) {
            // Format: "Street, Suburb, State Postcode" (3 parts) or more
            // For 3 parts: parts[0] is street, parts[1] is suburb
            // For more parts: join all but last 2 as street
            const streetAddress =
              parts.length === 3 ? parts[0] : parts.slice(0, -2).join(', ');
            const suburb = parts[parts.length - 2];
            const state = statePostcodeMatch[1];
            const postcode = statePostcodeMatch[2];

            update.$set['answers.user.address.streetAddress'] = streetAddress;
            update.$set['answers.user.address.suburb'] = suburb;
            update.$set['answers.user.address.state'] = state;
            update.$set['answers.user.address.postcode'] = postcode;
            update.$set['answers.user.address.full'] = answer;
            return;
          }

          // Try to extract state and postcode separately
          if (parts.length >= 4) {
            const stateMatch = /^([A-Z]{2,3})$/.exec(parts[parts.length - 2]);
            const postcodeMatch = /^(\d{4})$/.exec(parts[parts.length - 1]);

            if (stateMatch && postcodeMatch) {
              // Format: "Street, Suburb, State, Postcode" (4 parts) or more
              // For 4 parts: parts[0] is street, parts[1] is suburb
              // For more parts: join all but last 3 as street
              const streetAddress =
                parts.length === 4 ? parts[0] : parts.slice(0, -3).join(', ');
              const suburb = parts[parts.length - 3];
              const state = stateMatch[1];
              const postcode = postcodeMatch[1];

              update.$set['answers.user.address.streetAddress'] = streetAddress;
              update.$set['answers.user.address.suburb'] = suburb;
              update.$set['answers.user.address.state'] = state;
              update.$set['answers.user.address.postcode'] = postcode;
              update.$set['answers.user.address.full'] = answer;
              return;
            }
          }
        }

        // If all parsing attempts fail, throw error
        throw new BadRequestException(
          'Unable to parse address; please check the format. Expected format: "Street Address, Suburb, State Postcode" (e.g., "123 Main St, Sydney, NSW 2000")',
        );
      }

      // Use regex match results
      update.$set['answers.user.address.streetAddress'] =
        match.groups.street.trim();
      update.$set['answers.user.address.suburb'] = match.groups.suburb.trim();
      update.$set['answers.user.address.state'] = match.groups.state;
      update.$set['answers.user.address.postcode'] = match.groups.postcode;
      update.$set['answers.user.address.full'] = answer; // raw string
    },

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@magicmarc magicmarc merged commit a1b871c into main Dec 24, 2025
7 checks passed
@magicmarc magicmarc deleted the fix-onboarding branch December 24, 2025 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants