/*! * @license * Copyright 2019 Alfresco Software, Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewEncapsulation, OnDestroy } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { AuthenticationService } from '../../services/authentication.service'; import { LogService } from '../../services/log.service'; import { TranslationService } from '../../services/translation.service'; import { UserPreferencesService } from '../../services/user-preferences.service'; import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { LoginErrorEvent } from '../models/login-error.event'; import { LoginSubmitEvent } from '../models/login-submit.event'; import { LoginSuccessEvent } from '../models/login-success.event'; import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; import { OauthConfigModel } from '../../models/oauth-config.model'; import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; // eslint-disable-next-line no-shadow enum LoginSteps { Landing = 0, Checking = 1, Welcome = 2 } interface ValidationMessage { value: string; params?: any; } @Component({ selector: 'adf-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'], encapsulation: ViewEncapsulation.None, host: { class: 'adf-login' } }) export class LoginComponent implements OnInit, OnDestroy { isPasswordShow: boolean = false; /** * Should the `Remember me` checkbox be shown? When selected, this * option will remember the logged-in user after the browser is closed * to avoid logging in repeatedly. */ @Input() showRememberMe: boolean = true; /** Should the extra actions (`Need Help`, `Register`, etc) be shown? */ @Input() showLoginActions: boolean = true; /** Sets the URL of the NEED HELP link in the footer. */ @Input() needHelpLink: string = ''; /** Sets the URL of the REGISTER link in the footer. */ @Input() registerLink: string = ''; /** Path to a custom logo image. */ @Input() logoImageUrl: string = './assets/images/alfresco-logo.svg'; /** Path to a custom background image. */ @Input() backgroundImageUrl: string = './assets/images/background.svg'; /** The copyright text below the login box. */ @Input() copyrightText: string = '\u00A9 2016 Alfresco Software, Inc. All Rights Reserved.'; /** Custom validation rules for the login form. */ @Input() fieldsValidation: any; /** Route to redirect to on successful login. */ @Input() successRoute: string = null; /** Emitted when the login is successful. */ @Output() success = new EventEmitter(); /** Emitted when the login fails. */ @Output() error = new EventEmitter(); /** Emitted when the login form is submitted. */ @Output() executeSubmit = new EventEmitter(); implicitFlow: boolean = false; form: FormGroup; isError: boolean = false; errorMsg: string; actualLoginStep: any = LoginSteps.Landing; LoginSteps = LoginSteps; rememberMe: boolean = true; formError: { [id: string]: string }; minLength: number = 2; footerTemplate: TemplateRef; headerTemplate: TemplateRef; data: any; private _message: { [id: string]: { [id: string]: ValidationMessage } }; private onDestroy$ = new Subject(); constructor( private _fb: FormBuilder, private authService: AuthenticationService, private translateService: TranslationService, private logService: LogService, private router: Router, private appConfig: AppConfigService, private userPreferences: UserPreferencesService, private route: ActivatedRoute, private sanitizer: DomSanitizer, private alfrescoApiService: AlfrescoApiService ) { } ngOnInit() { this.initFormError(); this.initFormFieldsDefault(); this.initFormFieldsMessages(); this.successRoute = this.appConfig.get('successRoute', this.successRoute); if (this.authService.isLoggedIn()) { this.router.navigate([this.successRoute]); } else { if (this.authService.isOauth()) { const oauth: OauthConfigModel = this.appConfig.get(AppConfigValues.OAUTHCONFIG, null); if (oauth && oauth.silentLogin) { this.redirectToImplicitLogin(); } else if (oauth && oauth.implicitFlow) { this.implicitFlow = true; } } this.route.queryParams.subscribe((params: Params) => { const url = params['redirectUrl']; const provider = this.appConfig.get(AppConfigValues.PROVIDERS); this.authService.setRedirect({ provider, url }); }); } if (this.fieldsValidation) { this.form = this._fb.group(this.fieldsValidation); } this.form.valueChanges .pipe(takeUntil(this.onDestroy$)) .subscribe(data => this.onValueChanged(data)); } ngOnDestroy() { this.onDestroy$.next(true); this.onDestroy$.complete(); } submit() { this.onSubmit(this.form.value); } redirectToImplicitLogin() { this.alfrescoApiService.getInstance().oauth2Auth.implicitLogin(); } /** * Method called on submit form * * @param values * @param event */ onSubmit(values: any): void { this.disableError(); const args = new LoginSubmitEvent({ controls: { username: this.form.controls.username } }); this.executeSubmit.emit(args); if (!args.defaultPrevented) { this.performLogin(values); } } implicitLogin() { if (this.authService.isLoggedIn()) { this.router.navigate([this.successRoute]); } this.authService.ssoImplicitLogin(); } /** * The method check the error in the form and push the error in the formError object * * @param data */ onValueChanged(data: any) { this.disableError(); for (const field in this.formError) { if (field) { this.formError[field] = ''; const hasError = (this.form.controls[field].errors && data[field] !== '') || (this.form.controls[field].dirty && !this.form.controls[field].valid); if (hasError) { for (const key in this.form.controls[field].errors) { if (key) { const message = this._message[field][key]; if (message && message.value) { const translated = this.translateService.instant(message.value, message.params); this.formError[field] += translated; } } } } } } } private performLogin(values: any) { this.actualLoginStep = LoginSteps.Checking; this.authService .login(values.username, values.password, this.rememberMe) .subscribe( (token: any) => { const redirectUrl = this.authService.getRedirect(); this.actualLoginStep = LoginSteps.Welcome; this.userPreferences.setStoragePrefix(values.username); values.password = null; this.success.emit( new LoginSuccessEvent(token, values.username, null) ); if (redirectUrl) { this.authService.setRedirect(null); this.router.navigateByUrl(redirectUrl); } else if (this.successRoute) { this.router.navigate([this.successRoute]); } }, (err: any) => { this.actualLoginStep = LoginSteps.Landing; this.displayErrorMessage(err); this.isError = true; this.error.emit(new LoginErrorEvent(err)); }, () => this.logService.info('Login done') ); } /** * Check and display the right error message in the UI */ private displayErrorMessage(err: any): void { if ( err.error && err.error.crossDomain && err.error.message.indexOf('Access-Control-Allow-Origin') !== -1 ) { this.errorMsg = err.error.message; } else if ( err.status === 403 && err.message.indexOf('Invalid CSRF-token') !== -1 ) { this.errorMsg = 'LOGIN.MESSAGES.LOGIN-ERROR-CSRF'; } else if ( err.status === 403 && err.message.indexOf('The system is currently in read-only mode') !== -1 ) { this.errorMsg = 'LOGIN.MESSAGES.LOGIN-ECM-LICENSE'; } else { this.errorMsg = 'LOGIN.MESSAGES.LOGIN-ERROR-CREDENTIALS'; } } /** * Add a custom form error for a field * * @param field * @param msg */ public addCustomFormError(field: string, msg: string) { this.formError[field] += msg; } /** * Add a custom validation rule error for a field * * @param field * @param ruleId - i.e. required | minlength | maxlength * @param msg */ addCustomValidationError( field: string, ruleId: string, msg: string, params?: any ) { this._message[field][ruleId] = { value: msg, params }; } /** * Display and hide the password value. */ toggleShowPassword(event: Event) { event.stopPropagation(); this.isPasswordShow = !this.isPasswordShow; } /** * The method return if a field is valid or not * * @param field */ isErrorStyle(field: AbstractControl) { return !field.valid && field.dirty && !field.pristine; } /** * Trim username */ trimUsername(event: any) { event.target.value = event.target.value.trim(); } getBackgroundUrlImageUrl(): SafeStyle { return this.sanitizer.bypassSecurityTrustStyle(`url(${this.backgroundImageUrl})`); } /** * Default formError values */ private initFormError() { this.formError = { username: '', password: '' }; } /** * Init form fields messages */ private initFormFieldsMessages() { this._message = { username: { required: { value: 'LOGIN.MESSAGES.USERNAME-REQUIRED' }, minlength: { value: 'LOGIN.MESSAGES.USERNAME-MIN', params: { minLength: this.minLength } } }, password: { required: { value: 'LOGIN.MESSAGES.PASSWORD-REQUIRED' } } }; } private initFormFieldsDefault() { this.form = this._fb.group({ username: ['', Validators.compose([Validators.required, Validators.minLength(this.minLength)])], password: ['', Validators.required] }); } /** * Disable the error flag */ private disableError() { this.isError = false; this.initFormError(); } }