Auth Pages

src/app/views/pages/auth/ is the only feature area that renders outside BaseComponent. It has its own shell, AuthComponent, and is the module that registers the auth store slice.

The mechanics behind these screens — AuthService, the effects, the token — are documented in Authentication. This page covers the component layer.

Routes

All children of AuthComponent:

Path Component Guard Purpose

''

—

—

Redirects to login.

login

LoginComponent

LogOutGuard

Email + password sign-in.

auth

LoginAsSupplierComponent

—

Staff impersonation via a pre-issued token.

register

RegisterComponent

LogOutGuard

Self-service account creation.

reset-password

RecoverPasswordComponent

LogOutGuard

Request a reset email.

new-password

NewPasswordComponent

NewPasswordTokenGuard

Set a new password from an emailed code.

LogOutGuard keeps an already-authenticated user off these screens; NewPasswordTokenGuard validates the ?r= code before the form renders. See Route Guards.

The login route captures a return URL at module evaluation time
{
    path: 'login',
    component: LoginComponent,
    data: { returnUrl: window.location.pathname },
    canActivate: [LogOutGuard],
}

window.location.pathname is read once, when the route array is constructed — not per navigation. With hash routing the pathname is almost always /, so this data is effectively a constant. The value the component actually honours is the returnUrl query parameter, defaulting to /.

AuthModule

Unusually for a feature module, AuthModule owns store registration and several providers:

StoreModule.forFeature('auth', authReducer, AUTH_CONFIG_TOKEN),
EffectsModule.forFeature([AuthEffects, UserStatusEffects]),

AUTH_LOCAL_STORAGE_KEY

'auth_storage' — the localStorage key for the persisted slice.

AUTH_STORAGE_KEYS

['auth'] — which slices the meta-reducer persists.

AUTH_CONFIG_TOKEN

Factory that builds { metaReducers: [storageMetaReducer(…)] }.

RECAPTCHA_V3_SITE_KEY

environment.recaptchaKey.

forRoot() additionally provides AuthService, AuthGuard, DashboardStatusGuard, LogOutGuard and NewPasswordTokenGuard — which is why AppModule imports AuthModule.forRoot() even though the routes are lazy.

The auth slice is registered by a lazy module. Anything that selects from auth before the auth bundle has loaded gets undefined. In practice the module is always loaded, because forRoot() pulls it into the main bundle — but do not rely on the slice existing in an isolated test without importing AuthModule.

Input controls come from @payability/pyb-ngx-components (PybTextInputModule, PybPasswordInputModule, PybFormFieldModule, PybLabelModule, PybErrorModule, PybStandardButtonModule) rather than raw Material inputs. RecaptchaComponent is declared here directly from core/pyb/components/recaptcha/.

Shared field constraints

auth.enum.ts and auth.const.ts hold the control names and length bounds shared by login and registration:

export const pybAuthControlLength = new Map<PybAuthControls, PybMinMax>([
    [PybAuthControls.FirstName,   { min: 3, max: 100 }],
    [PybAuthControls.LastName,    { min: 3, max: 100 }],
    [PybAuthControls.CompanyName, { min: 3, max: 200 }],
    [PybAuthControls.Phone,       { min: 5, max: 15  }],
    [PybAuthControls.Email,       { min: 3, max: 320 }],
    [PybAuthControls.Password,    { min: 8, max: 100 }],
]);

Both forms build validators from this map rather than repeating numbers, so changing a bound here changes both screens.

LoginComponent

submit() chains reCAPTCHA, login, and a host check:

this.recaptchaV3Service.execute('login').pipe(
    switchMap(recaptcha =>
        this.auth.login(authData.email, authData.password, recaptcha).pipe(
            catchError(err => {
                if (err.error?.error_code !== 'ACCOUNT_LOCKED') {                      (1)
                    this.authNoticeService.setNotice(this.translate.instant('AUTH.VALIDATION.INVALID_LOGIN'), 'danger');
                }
                this.loading = false;
                return throwError(err);
            }),
            map(data => {
                const { supplier_site: supplierSite } = jwtDecode(data.token);         (2)
                const currentHost = window.location.host;
                if (!this.isLocalHost() && !this.dashboardPageHelperService.isOnCorrectSite(supplierSite, currentHost)) {
                    this.dashboardPageHelperService.redirectToSupplierSite(supplierSite, currentHost, token);
                    this.unsubscribe.next();
                    return;                                                            (3)
                }
                this.store.dispatch(login({ authToken: data.token }));
            }),
        ),
    ),
).subscribe();
1 A locked account is deliberately not given the generic invalid-credentials notice. HttpErrorHandlerInterceptor already matches 401 + error_code === 'ACCOUNT_LOCKED' and raises a 30-second toast telling the supplier to email support; a "wrong email or password" banner underneath it would contradict the real reason.
2 The JWT is decoded client-side to read supplier_site, before any store dispatch.
3 If the supplier belongs to a different host, the component redirects there carrying the token and never dispatches login. The session therefore starts on the correct host. Skipped on localhost.

The reCAPTCHA action name is 'login'; the recovery screen uses 'reset_password_start'. See reCAPTCHA.

There is no route for the marketing sign-up flow, so the link is computed from the current URL:

get siteVerision(): string {                        (1)
    let regex, replaceValue;
    if (this.isNewEggPage()) {
        regex = /neweggcapital2/gi;
        replaceValue = 'newegg-onboarding';
    } else {
        regex = /supplier\d?/gi;
        replaceValue = 'onboarding';
    }
    return window.location.href.replace(regex, replaceValue).split('.')[0];
}

get createAccountUrl(): string {
    if (this.isLocalHost()) {
        return '#/auth/register';
    }
    return `${this.siteVerision}.${this.topLevelDomain}/#/login/create`;
}
1 Misspelled in the source (siteVerision).

So supplier.payability.com sends new users to onboarding.payability.com, and a NewEgg-branded host sends them to newegg-onboarding…. topLevelDomain picks dev.payability.com when the URL contains .dev.

/auth/register is reachable only on localhost through this link. In every deployed environment the button leaves the application, so RegisterComponent is effectively dev-only UI even though the route is live. Anyone who navigates directly to #/auth/register in production still gets a working form.

loginViaSocialMediaIsAvailable is a hardcoded false that hides the social buttons inherited from the original template.

RegisterComponent

Builds a seven-control form with a cross-field validator:

this.registerForm = this.fb.group({ /* … */ }, {
    validator: ConfirmPasswordValidator.MatchPassword,
});

firstName, lastName

Required, 3–100, pattern [A-Z]{1,1}[a-z]+.

companyName

Required, 3–200.

phone

Required, 5–15, pattern ^\+)|(00)|(\*)|([0-9]{5,15}$.

email

Required, Validators.email, 3–320, plus an additional pattern.

password, confirmPassword

Required, 8–100, matched by the group validator.

agree

Validators.required on a boolean.

Three validation quirks worth knowing
  • The name pattern [A-Z]{1,1}[a-z]+ is unanchored, so it demands that a capital-then-lowercase sequence appear somewhere — o’Brian passes, MCDONALD fails.

  • The email pattern ends \.[a-z]{2,3}, which rejects any four-letter TLD: .info, .name, .shop. Validators.email alone would have accepted them.

  • Validators.required on the agree checkbox does not work — false is a present value. The component compensates with an explicit check in submit(), which sets a notice but returns without resetting loading, leaving the button in its loading state.

ConfirmPasswordValidator.MatchPassword sets the error on the control rather than returning it:

static MatchPassword(control: AbstractControl) {
    const password = control.get('password').value;
    const confirmPassword = control.get('confirmPassword').value;
    if (password !== confirmPassword) {
        control.get('confirmPassword').setErrors({ ConfirmPassword: true });
    } else {
        return null;
    }
}

It returns undefined in the failure branch, so the group stays valid while the confirmPassword control carries a ConfirmPassword error. Templates must check the control, not the form. The mismatch branch also overwrites any other errors on that control.

Submission builds a User model by hand, calls AuthService.register() and dispatches register({ authToken, userId }).

RecoverPasswordComponent — /auth/reset-password

A single email field, dispatching recoverPasswordEmailRequest({ email, token }) after a reCAPTCHA execution. State comes from the store: selectIsPendingRecoverPasswordRequest, selectIsRecoverPasswordRequestSent, selectRecoverPasswordRequestError.

A backend failure is translated into a field-level error rather than a banner:

this.recoverPasswordRequestError$.pipe(takeUntil(this.destroyed.asObservable())).subscribe(() => {
    this.formGroup.controls.email.setErrors({ emailNotFound: true });
});

The screen has a second mode. With ?required=<email> the address is pre-filled and disabled, and isPasswordChangeRequest is set — used when the backend forces a password change for a known account:

email: [
    !!defaultEmail ? decodeURIComponent(defaultEmail) : '',
    [Validators.required, Validators.email, Validators.minLength(3), Validators.maxLength(320)],
]

ngOnDestroy calls this.captchaSub$.unsubscribe() unconditionally, but captchaSub$ is only assigned in submitClick(). Leaving the page without submitting throws a TypeError on destroy. It is swallowed by Angular’s error handler and does not break navigation, but it does appear in the console and in Sentry-style error reporting.

NewPasswordComponent — /auth/new-password

Reached from the reset email, which carries ?r=<verificationCode>. NewPasswordTokenGuard has already confirmed the code by the time the form renders; the component re-reads it from the snapshot on submit:

const { password } = this.formGroup.value;
const { r: verificationCode } = this.activatedRoute.snapshot.queryParams;
this.store.dispatch(newPasswordRequest({ password, verificationCode }));

Password rules and error copy live in new-password.constants.ts:

export const passwordValidationPattern = /(?=.*?[A-Z])(?=.*?[0-9]).{8,}/;

export const formErrorsMap: Dictionary<string> = {
    required:        'AUTH.VALIDATION.REQUIRED_FIELD',
    pattern:         'AUTH.VALIDATION.PASSWORD_INVALID',
    ConfirmPassword: 'AUTH.VALIDATION.PASSWORDS_NOT_MATCHING',
};

At least one uppercase letter, at least one digit, minimum eight characters. No symbol is required and none is forbidden. ConfirmPassword in the error map is the key set by ConfirmPasswordValidator, reused here from the register folder.

Success navigates to login and raises a BannerTypes.SUCCESS banner; failure raises BannerTypes.DANGER. ngOnDestroy dispatches newPasswordRequestReset() so a revisit starts clean.

new-password-form/ and recover-password-form/ are dumb presentational bodies, each taking [formGroup] and [errorsMap] inputs and used by exactly one parent:

<pyb-new-password-form [formGroup]="formGroup" [errorsMap]="errorsMap"></pyb-new-password-form>

The split keeps the store wiring in the routed component and the markup testable on its own. Neither is reused outside AuthModule, so a change to either affects one screen only.

LoginAsSupplierComponent — /auth/auth

The impersonation and cross-host landing target. It does one thing:

this.activatedRoute.queryParams.subscribe(params => {
    const authToken = params.token;
    this.store.dispatch(login({ authToken }));
});

Then waits for selectIsUserLoaded and navigates to /. It is also where LoginComponent’s cross-host redirect lands, since `redirectToSupplierSite() builds …/#/login/auth?token=<token>.

There is no validation of params.token. A missing or malformed token is dispatched anyway; the failure surfaces later, when getUserByToken() returns 401 and UnauthorizedInterceptor bounces the user to login. The whole session therefore hinges on a URL query parameter — treat impersonation links as credentials.

AuthNoticeService and AuthNoticeComponent

The banner mechanism for these screens, separate from the authenticated BannerService. AuthNoticeService.setNotice(message, type) pushes to onNoticeChanged$; pyb-auth-notice renders it.

Notices survive a navigation within the auth area, which is how registration success is shown on the login screen:

this.authNoticeService.setNotice(this.translate.instant('AUTH.REGISTER.SUCCESS'), 'success');

LoginComponent clears the notice in both ngOnInit and ngOnDestroy, so a notice set before navigating to login survives, but one set earlier in the session does not linger.

AuthNoticeComponent declares message and type as @Output() although they are consumed as internal state — a template-era artefact. They are not real outputs; nothing binds to them.