- Enhanced UnifiedExceptionFilter to handle ZodValidationException, extracting field errors for better user feedback. - Updated WhmcsRequestQueueService and WhmcsHttpClientService to use logger.warn for non-critical errors, improving log clarity. - Introduced Redis-backed caching in JapanPostFacade for postal code lookups, reducing redundant API calls. - Refactored address handling in AddressWriterService to deduplicate concurrent Japan Post lookups, optimizing API usage. - Improved error parsing in various forms and hooks to provide clearer error messages and field-specific feedback.
12 KiB
Auth Flow Simplification Design
Date: 2026-03-03 Status: Approved
Problem
The auth system has accumulated duplication and dead code:
- Duplicate signup workflows:
SfCompletionWorkflowandNewCustomerSignupWorkfloware 85% identical (~300 lines each). The only difference is eligibility case creation and a welcome email. - Confusing names:
SfCompletionWorkflowdoesn't describe what it does (it creates full accounts, not just "completes SF"). - Missing SF fields:
BirthdateandSex__care not set on Salesforce PersonContact during account creation. The only code that calledcreateContact()lived in dead legacy code. - Dead code:
signup/directory (6 files),WhmcsLinkWorkflowService— all unreferenced or superseded. - Legacy duplicate:
WhmcsLinkWorkflowService(password-based migration viaPOST /auth/migrate) is superseded byWhmcsMigrationWorkflowService(OTP-based migration via get-started). - Scattered shared services:
SignupUserCreationServiceandSignupWhmcsServicelive insignup/but are used by other workflows viaCreatePortalUserStep. They should live alongside the steps they serve.
Approach — 3 Phases (can stop after any phase)
Phase 1: Merge signup workflows + cleanup
Merge the two near-duplicate workflows. Move shared services. Delete dead code. Fix Birthdate/Sex__c.
Phase 2: Remove legacy migrate endpoint
Delete WhmcsLinkWorkflowService and POST /auth/migrate. All WHMCS migration goes through the get-started OTP-based flow.
Phase 3: Clean up OTP infrastructure
Extract shared OTP email-sending pattern. Login and signup OTP stay as separate workflows (they serve different purposes: 2FA vs email verification). Keep separate sessions (different data models, TTLs, lifecycle). Improve the PORTAL_EXISTS redirect UX (prefill email on login page).
Why login and signup stay separate: In the ISP/telecom industry, login (password → OTP as 2FA) and signup (OTP as email verification → form) serve fundamentally different security purposes. Forcing them into one flow either adds friction for existing users or weakens the security model. Major ISP portals keep these flows separate.
Current Structure
workflows/
├── verification-workflow.service.ts # OTP + account status detection
├── guest-eligibility-workflow.service.ts # Guest ISP check (no account created)
├── sf-completion-workflow.service.ts # Creates account (misleading name, 85% overlap)
├── new-customer-signup-workflow.service.ts # Creates account + eligibility (85% overlap)
├── whmcs-migration-workflow.service.ts # OTP-based WHMCS migration (active)
├── whmcs-link-workflow.service.ts # Password-based WHMCS migration (legacy, superseded)
├── login-otp-workflow.service.ts # Login OTP 2FA
├── password-workflow.service.ts # Password operations
├── get-started-coordinator.service.ts # Get-started routing
├── workflow-error.util.ts # Error classification
├── signup/ # Mixed: some active, some dead
│ ├── signup-account-resolver.service.ts # Dead (no active caller)
│ ├── signup-validation.service.ts # Dead (no active caller)
│ ├── signup-whmcs.service.ts # Active (markClientForCleanup used by CreatePortalUserStep)
│ ├── signup-user-creation.service.ts # Active (used by CreatePortalUserStep + WhmcsMigration)
│ ├── signup.types.ts # Dead (only used by dead validation service)
│ └── index.ts # Barrel
└── steps/ # Shared step services
├── resolve-salesforce-account.step.ts
├── create-whmcs-client.step.ts
├── create-portal-user.step.ts # Delegates to SignupUserCreationService
├── create-eligibility-case.step.ts
├── update-salesforce-flags.step.ts
├── generate-auth-result.step.ts
└── index.ts
New Structure (after all 3 phases)
workflows/
├── verification-workflow.service.ts # OTP + account detection (unchanged)
├── guest-eligibility-workflow.service.ts # Guest ISP check (unchanged)
├── account-creation-workflow.service.ts # MERGED: replaces sf-completion + new-customer-signup
├── account-migration-workflow.service.ts # RENAMED: from whmcs-migration
├── login-otp-workflow.service.ts # Login OTP 2FA (Phase 3: uses shared OTP orchestration)
├── password-workflow.service.ts # Unchanged
├── get-started-coordinator.service.ts # Updated imports
├── workflow-error.util.ts # Unchanged
└── steps/ # Shared steps + moved services
├── resolve-salesforce-account.step.ts
├── create-whmcs-client.step.ts
├── create-portal-user.step.ts
├── create-eligibility-case.step.ts
├── update-salesforce-flags.step.ts
├── generate-auth-result.step.ts
├── portal-user-creation.service.ts # MOVED from signup/ (renamed)
├── whmcs-cleanup.service.ts # MOVED from signup/ (renamed, trimmed)
└── index.ts
Deleted (all phases combined):
sf-completion-workflow.service.ts(merged, Phase 1)new-customer-signup-workflow.service.ts(merged, Phase 1)whmcs-migration-workflow.service.ts(renamed, Phase 1)whmcs-link-workflow.service.ts(removed, Phase 2)signup/entire directory (Phase 1)
Phase 1: Merge signup workflows + cleanup
Merged Workflow: AccountCreationWorkflowService
API:
class AccountCreationWorkflowService {
execute(
request: CompleteAccountRequest | SignupWithEligibilityRequest,
options?: { withEligibility?: boolean }
): Promise<AuthResultInternal | SignupWithEligibilityResult>;
}
Routing:
POST /auth/get-started/complete-account→execute(request)(no eligibility)POST /auth/get-started/signup-with-eligibility→execute(request, { withEligibility: true })
Step Sequence:
1. Validate + check existing accounts
2. Hash password
3. Resolve address + names (from request or session prefill)
4. Step 1: Resolve SF Account (CRITICAL)
5. Step 1.5: Write SF Address (DEGRADABLE)
6. Step 1.6: Create SF Contact — Birthdate + Sex__c (DEGRADABLE) ← NEW, fixes the bug
7. [if withEligibility] Step 2: Create Eligibility Case (DEGRADABLE)
8. Step 3: Create WHMCS Client (CRITICAL, rollback)
9. Step 4: Create Portal User (CRITICAL, rollback WHMCS on fail)
10. Step 5: Update SF Flags (DEGRADABLE)
11. Step 6: Generate Auth Result
12. [if withEligibility] Send welcome email with eligibility ref (DEGRADABLE)
13. Invalidate session
Error handling difference between paths:
completeAccountpath: throws exceptions directly (current sf-completion behavior)signupWithEligibilitypath: wraps in try/catch, returns{ success: false, message, errorCategory }usingclassifyError()
Birthdate/Sex__c Fix:
After resolving the SF account, call SalesforceAccountService.createContact() on the PersonContact:
Birthdate→dateOfBirth(YYYY-MM-DD)Sex__c→ mapped fromgender(male→男性,female→女性,other→Other) This is degradable — account creation continues if it fails.
Rename: AccountMigrationWorkflowService
Rename WhmcsMigrationWorkflowService → AccountMigrationWorkflowService. Refactor to use CreatePortalUserStep instead of calling SignupUserCreationService directly (reuse existing step, same as other workflows).
Move signup/ services into steps/
| Old | New | Changes |
|---|---|---|
signup/signup-user-creation.service.ts |
steps/portal-user-creation.service.ts |
Rename class to PortalUserCreationService |
signup/signup-whmcs.service.ts |
steps/whmcs-cleanup.service.ts |
Rename to WhmcsCleanupService, keep only markClientForCleanup() |
Delete dead code
| File | Reason |
|---|---|
signup/signup-account-resolver.service.ts |
No active caller |
signup/signup-validation.service.ts |
No active caller |
signup/signup.types.ts |
Only used by dead validation service |
signup/index.ts |
Directory being removed |
Phase 2: Remove legacy migrate endpoint
Delete WhmcsLinkWorkflowService
WhmcsLinkWorkflowService (POST /auth/migrate) is superseded by AccountMigrationWorkflowService (POST /auth/get-started/migrate-whmcs-account). The new flow uses OTP-based identity verification instead of WHMCS password validation — strictly more secure.
Files to modify:
apps/bff/src/modules/auth/auth.module.ts— remove provider + importapps/bff/src/modules/auth/application/auth-orchestrator.service.ts— removelinkWhmcsUser()method + dependencyapps/bff/src/modules/auth/presentation/http/auth.controller.ts— removePOST /auth/migrateendpoint
File to delete:
apps/bff/src/modules/auth/infra/workflows/whmcs-link-workflow.service.ts
Phase 3: Clean up OTP infrastructure
Extract shared OTP email-sending pattern
Both LoginOtpWorkflowService and VerificationWorkflowService implement the same pattern:
- Look up email template ID from config
- If template exists, send via template with dynamic data
- If no template, send fallback HTML
Extract this into a shared OtpEmailService (or method on existing OtpService).
Improve PORTAL_EXISTS redirect UX
When get-started verification detects PORTAL_EXISTS, pass the verified email to the login page so it's prefilled. This reduces friction for users who accidentally start on the wrong page.
What stays separate (and why)
- Login OTP workflow: OTP serves as 2FA after password validation
- Verification OTP workflow: OTP serves as email ownership proof before signup
- LoginSessionService: Simple (5 fields, 10min TTL, no prefill)
- GetStartedSessionService: Complex (10+ fields, 1hr TTL, prefill data, handoff tokens, idempotency locking)
These serve fundamentally different security purposes and have different data models. Forcing unification would create a confusing abstraction.
Module Updates (all phases)
GetStartedModule (Phase 1)
- Remove:
NewCustomerSignupWorkflowService,SfCompletionWorkflowService,WhmcsMigrationWorkflowService - Remove:
SignupAccountResolverService,SignupValidationService,SignupWhmcsService,SignupUserCreationService - Add:
AccountCreationWorkflowService,AccountMigrationWorkflowService - Add:
PortalUserCreationService,WhmcsCleanupService
GetStartedCoordinator (Phase 1)
completeAccount()→accountCreation.execute(request)signupWithEligibility()→accountCreation.execute(request, { withEligibility: true })migrateWhmcsAccount()→accountMigration.execute(request)
AuthModule (Phase 2)
- Remove:
WhmcsLinkWorkflowServicefrom providers and imports
AuthOrchestrator (Phase 2)
- Remove:
linkWhmcsUser()method andWhmcsLinkWorkflowServicedependency
AuthController (Phase 2)
- Remove:
POST /auth/migrateendpoint