cloudfoundry/stratos

View on GitHub
src/frontend/packages/core/src/features/login/login-page/login-page.component.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { Component, OnDestroy, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Store } from '@ngrx/store';
import { Observable, Subscription } from 'rxjs';
import { map, startWith, takeWhile, tap } from 'rxjs/operators';

import { Login, VerifySession } from '../../../../../store/src/actions/auth.actions';
import { RouterNav } from '../../../../../store/src/actions/router.actions';
import { InternalAppState } from '../../../../../store/src/app-state';
import { AuthState } from '../../../../../store/src/reducers/auth.reducer';
import { RouterRedirect } from '../../../../../store/src/reducers/routing.reducer';
import { queryParamMap } from '../../../core/auth-guard.service';

@Component({
  selector: 'app-login-page',
  templateUrl: './login-page.component.html',
  styleUrls: ['./login-page.component.scss']
})
export class LoginPageComponent implements OnInit, OnDestroy {

  constructor(
    private store: Store<Pick<InternalAppState, 'endpoints' | 'auth'>>
  ) { }

  loginForm: NgForm;

  username: string;
  password: string;

  loggedIn: boolean;
  loggingIn: boolean;
  isLoginFlow: boolean;
  verifying: boolean;
  error: boolean;

  ssoLogin: boolean;
  ssoOptions: string;

  busy$: Observable<boolean>;
  initialLoad$: Observable<boolean>;

  redirect: RouterRedirect;

  message = '';

  subscription: Subscription;

  showPassword = false;

  ngOnInit() {
    this.ssoLogin = false;
    this.store.dispatch(new VerifySession());
    const auth$ = this.store.select(s => ({ auth: s.auth, endpoints: s.endpoints }));
    this.initialLoad$ = auth$.pipe(
      map(({ auth, }) =>
        auth.verifying && !auth.loggingIn || // checking if user is logged in but not in processed of logging in
        (auth.sessionData && auth.sessionData.valid) && !this.isLoginFlow // logged in but haven't hit log in
      ),
    );

    this.busy$ = auth$.pipe(
      map(({ auth, endpoints }) =>
        !auth.error ||
        !(auth.sessionData && auth.sessionData.valid) && (auth.sessionData && auth.sessionData.valid) ||
        auth.verifying ||
        auth.loggingIn ||
        endpoints.loading
      ),
      startWith(true)
    );
    this.subscription = auth$.pipe(
      tap(({ auth }) => {
        this.redirect = auth.redirect;
        this.handleOther(auth);
      }),
      takeWhile(({ auth }) => {
        const loggedIn = !auth.loggingIn && auth.loggedIn;
        const validSession = auth.sessionData && auth.sessionData.valid;
        return !(loggedIn && validSession);
      }),
    )
      .subscribe({
        complete: () => this.handleSuccess()
      });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  formSSOredirectURL() {
    const queryKeys = this.redirect ? Object.keys(this.redirect.queryParams) : undefined;
    return window.location.protocol + '//' + window.location.hostname +
      (window.location.port ? ':' + window.location.port : '') +
      (this.redirect ?
        this.redirect.path +
        (queryKeys.length > 0 ? '?' + queryKeys.map(k => k + '=' + this.redirect.queryParams[k]).join('&') : '') : '/');
  }

  login() {
    if (this.ssoLogin) {
      return this.doSSOLogin();
    }
    this.message = '';
    this.store.dispatch(new Login(this.username, this.password));
  }

  private handleSuccess() {
    if (this.subscription) {
      this.subscription.unsubscribe(); // Ensure to unsub otherwise GoToState gets caught in loop
    }
    if (this.redirect) {
      this.store.dispatch(new RouterNav({ path: [decodeURI(this.redirect.path)], query: this.redirect.queryParams || {} }));
    } else {
      this.store.dispatch(new RouterNav({ path: ['/'] }, null));
    }
  }

  private handleOther(auth: AuthState) {
    this.loggedIn = auth.loggedIn;
    this.loggingIn = auth.loggingIn;
    this.isLoginFlow = this.isLoginFlow || auth.loggingIn;
    this.verifying = auth.verifying;
    this.ssoOptions = auth.sessionData && auth.sessionData.ssoOptions;
    this.ssoLogin = !!this.ssoOptions;

    const params = queryParamMap();
    const ssoMessage = params.SSO_Message;

    // Upgrade in progress
    if (auth.sessionData && auth.sessionData.upgradeInProgress) {
      this.subscription.unsubscribe(); // Ensure to unsub otherwise GoToState gets caught in loop
      this.store.dispatch(new RouterNav({ path: ['/upgrade'], extras: { skipLocationChange: true } }));
      return false;
    }

    // Setup mode
    if (auth.sessionData && auth.sessionData.uaaError) {
      this.subscription.unsubscribe(); // Ensure to unsub otherwise GoToState gets caught in loop
      this.store.dispatch(new RouterNav({ path: ['/setup'] }));
      return false;
    }

    // Cookie domain mismatch (user won't be able to login)
    if (auth.sessionData && auth.sessionData.domainMismatch) {
      this.subscription.unsubscribe(); // Ensure to unsub otherwise GoToState gets caught in loop
      this.store.dispatch(new RouterNav({ path: ['/domainMismatch'], extras: { skipLocationChange: true } }));
      return false;
    }

    // Check for SSO Login without splash page - i.e. redirect straight to SSO Login UI
    if (!this.loggedIn && this.ssoNoSplashPage() && !ssoMessage) {
      return this.doSSOLogin();
    }

    // auth.sessionData will be populated if user has been redirected here after attempting to access a protected page without
    // a valid session
    this.error = auth.error && (!auth.sessionData || !auth.sessionData.valid) && !this.ssoLogin;

    if (this.error) {
      this.setErrorMessage(auth);
    }

    if (!!ssoMessage) {
      this.message = ssoMessage;
    }
  }

  private setErrorMessage(auth: AuthState) {
    // Default error message
    this.message = `Couldn't log in, please try again.`;
    if (auth.error && auth.errorResponse) {
      if (auth.errorResponse === 'Invalid session') {
        // Invalid session (redirected after attempting to access a protected page). Don't show any error
        this.message = '';
      } else if (auth.errorResponse.status === 401) {
        // User supplied invalid credentials
        this.message = 'Username and password combination incorrect. Please try again.';
        // Is there a better error message available? e.g. account locked
        const authError = !!auth.errorResponse.error ? auth.errorResponse.error.error : null;
        if (!!authError && authError !== 'Bad credentials') {
          this.message = authError;
        }
      } else if (auth.errorResponse.status >= 500 && auth.errorResponse.status < 600) {
        this.message = `Couldn't check credentials, please try again. If the problem persists please contact an administrator`;
      }
    }
  }

  private ssoNoSplashPage() {
    return this.ssoLogin && this.ssoOptions.indexOf('nosplash') >= 0;
  }

  private doSSOLogin() {
    const returnUrl = encodeURIComponent(this.formSSOredirectURL());
    window.open('/pp/v1/auth/sso_login?state=' + returnUrl, '_self');
    this.busy$ = new Observable<boolean>((observer) => {
      observer.next(true);
      observer.complete();
    });
  }

}