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 |
|
|
|
Email + password sign-in. |
|
|
— |
Staff impersonation via a pre-issued token. |
|
|
|
Self-service account creation. |
|
|
|
Request a reset email. |
|
|
|
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
|
AuthModule
Unusually for a feature module, AuthModule owns store registration and several
providers:
StoreModule.forFeature('auth', authReducer, AUTH_CONFIG_TOKEN),
EffectsModule.forFeature([AuthEffects, UserStatusEffects]),
|
|
|
|
|
Factory that builds |
|
|
forRoot() additionally provides AuthService, AuthGuard,
DashboardStatusGuard, LogOutGuard and NewPasswordTokenGuard — which is why
AppModule imports AuthModule.forRoot() even though the routes are lazy.
|
The |
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.
The create-account link is host-derived
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.
|
|
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,
});
|
Required, 3–100, pattern |
|
Required, 3–200. |
|
Required, 5–15, pattern |
|
Required, |
|
Required, 8–100, matched by the group validator. |
|
|
|
Three validation quirks worth knowing
|
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)],
]
|
|
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 |
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.
|
|
Related pages
-
Authentication — services, effects, token handling
-
Auth Slice — actions and selectors used above
-
Route Guards —
LogOutGuard,NewPasswordTokenGuard -
Two-Factor Authentication — the challenge that follows login