[ADF-578] Remember me functionality (#1962)

* Remember me functionality

* Happy pack to ng2-components' package.json

* Build fix
This commit is contained in:
Popovics András
2017-06-14 15:47:08 +01:00
committed by Eugenio Romano
parent 75778ab968
commit fb4af7f846
19 changed files with 288 additions and 31 deletions

View File

@@ -29,6 +29,7 @@ import {
AlfrescoContentService,
AlfrescoSettingsService,
StorageService,
CookieService,
AlfrescoApiService,
AlfrescoTranslateLoader,
AlfrescoTranslationService,
@@ -69,6 +70,7 @@ export const ALFRESCO_CORE_PROVIDERS: any[] = [
AlfrescoContentService,
AlfrescoSettingsService,
StorageService,
CookieService,
AlfrescoApiService,
AlfrescoTranslateLoader,
AlfrescoTranslationService,

View File

@@ -0,0 +1,27 @@
/*!
* @license
* Copyright 2016 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.
*/
export class CookieServiceMock {
getItem(key: string): string | null {
return this[key] && this[key].data || null;
}
setItem(key: string, data: string, expiration: Date | null, path: string | null): void {
this[key] = {data, expiration, path};
}
}

View File

@@ -20,6 +20,8 @@ import { AlfrescoSettingsService } from './alfresco-settings.service';
import { AlfrescoAuthenticationService } from './alfresco-authentication.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { StorageService } from './storage.service';
import { CookieService } from './cookie.service';
import { CookieServiceMock } from './../assets/cookie.service.mock';
import { LogService } from './log.service';
declare let jasmine: any;
@@ -29,6 +31,7 @@ describe('AlfrescoAuthenticationService', () => {
let authService: AlfrescoAuthenticationService;
let settingsService: AlfrescoSettingsService;
let storage: StorageService;
let cookie: CookieService;
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
@@ -36,11 +39,13 @@ describe('AlfrescoAuthenticationService', () => {
AlfrescoApiService,
AlfrescoAuthenticationService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService
]);
authService = injector.get(AlfrescoAuthenticationService);
settingsService = injector.get(AlfrescoSettingsService);
cookie = injector.get(CookieService);
storage = injector.get(StorageService);
storage.clear();
@@ -51,6 +56,64 @@ describe('AlfrescoAuthenticationService', () => {
jasmine.Ajax.uninstall();
});
describe('remembe me', () => {
beforeEach(() => {
settingsService.setProviders('ECM');
});
it('should save the remember me cookie as a session cookie after successful login', (done) => {
authService.login('fake-username', 'fake-password', false).subscribe(() => {
expect(cookie['ALFRESCO_REMEMBER_ME']).not.toBeUndefined();
expect(cookie['ALFRESCO_REMEMBER_ME'].expiration).toBeNull();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 201,
contentType: 'application/json',
responseText: JSON.stringify({'entry': {'id': 'fake-post-ticket', 'userId': 'admin'}})
});
});
it('should save the remember me cookie as a persistent cookie after successful login', (done) => {
authService.login('fake-username', 'fake-password', true).subscribe(() => {
expect(cookie['ALFRESCO_REMEMBER_ME']).not.toBeUndefined();
expect(cookie['ALFRESCO_REMEMBER_ME'].expiration).not.toBeNull();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 201,
contentType: 'application/json',
responseText: JSON.stringify({'entry': {'id': 'fake-post-ticket', 'userId': 'admin'}})
});
});
it('should not save the remember me cookie after failed login', (done) => {
authService.login('fake-username', 'fake-password').subscribe(
(res) => {},
(err: any) => {
expect(cookie['ALFRESCO_REMEMBER_ME']).toBeUndefined();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 403,
contentType: 'application/json',
responseText: JSON.stringify({
'error': {
'errorKey': 'Login failed',
'statusCode': 403,
'briefSummary': '05150009 Login failed',
'stackTrace': 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.',
'descriptionURL': 'https://api-explorer.alfresco.com'
}
})
});
});
});
describe('when the setting is ECM', () => {
beforeEach(() => {

View File

@@ -19,23 +19,29 @@ import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs/Rx';
import { AlfrescoSettingsService } from './alfresco-settings.service';
import { StorageService } from './storage.service';
import { CookieService } from './cookie.service';
import { LogService } from './log.service';
import { AlfrescoApiService } from './alfresco-api.service';
const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME';
const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30 ;
@Injectable()
export class AlfrescoAuthenticationService {
onLogin: Subject<any> = new Subject<any>();
onLogout: Subject<any> = new Subject<any>();
constructor(private settingsService: AlfrescoSettingsService,
constructor(
private settingsService: AlfrescoSettingsService,
public alfrescoApi: AlfrescoApiService,
private storage: StorageService,
private cookie: CookieService,
private logService: LogService) {
}
/**
* The method return tru if the user is logged in
* The method return true if the user is logged in
* @returns {boolean}
*/
isLoggedIn(): boolean {
@@ -48,10 +54,11 @@ export class AlfrescoAuthenticationService {
* @param password
* @returns {Observable<R>|Observable<T>}
*/
login(username: string, password: string): Observable<{ type: string, ticket: any }> {
login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string, ticket: any }> {
this.removeTicket();
return Observable.fromPromise(this.callApiLogin(username, password))
.map((response: any) => {
this.saveRememberMeCookie(rememberMe);
this.saveTickets();
this.onLogin.next(response);
return { type: this.settingsService.getProviders(), ticket: response };
@@ -59,6 +66,31 @@ export class AlfrescoAuthenticationService {
.catch(err => this.handleError(err));
}
/**
* The method save the "remember me" cookie as a long life cookie or a session cookie
* depending on the given paramter
*/
private saveRememberMeCookie(rememberMe: boolean): void {
let expiration = null;
if (rememberMe) {
expiration = new Date();
const time = expiration.getTime();
const expireTime = time + REMEMBER_ME_UNTIL;
expiration.setTime(expireTime);
}
this.cookie.setItem(REMEMBER_ME_COOKIE_KEY, '1', expiration, null);
}
/**
* The method retrieve whether the "remember me" cookie was set or not
*
* @returns {boolean}
*/
private isRememberMeSet(): boolean {
return (this.cookie.getItem(REMEMBER_ME_COOKIE_KEY) === null) ? false : true;
}
/**
* Initialize the alfresco Api with user and password end call the login method
* @param username
@@ -130,7 +162,7 @@ export class AlfrescoAuthenticationService {
/**
* The method save the ECM and BPM ticket in the Storage
*/
saveTickets() {
saveTickets(): void {
this.saveTicketEcm();
this.saveTicketBpm();
}
@@ -155,16 +187,20 @@ export class AlfrescoAuthenticationService {
/**
* The method return true if user is logged in on ecm provider
*
* @returns {boolean}
*/
isEcmLoggedIn() {
return this.alfrescoApi.getInstance().ecmAuth && !!this.alfrescoApi.getInstance().ecmAuth.isLoggedIn();
isEcmLoggedIn(): boolean {
return this.isRememberMeSet() && this.alfrescoApi.getInstance().ecmAuth && !!this.alfrescoApi.getInstance().ecmAuth.isLoggedIn();
}
/**
* The method return true if user is logged in on bpm provider
*
* @returns {boolean}
*/
isBpmLoggedIn() {
return this.alfrescoApi.getInstance().bpmAuth && !!this.alfrescoApi.getInstance().bpmAuth.isLoggedIn();
isBpmLoggedIn(): boolean {
return this.isRememberMeSet() && this.alfrescoApi.getInstance().bpmAuth && !!this.alfrescoApi.getInstance().bpmAuth.isLoggedIn();
}
/**

View File

@@ -21,6 +21,8 @@ import { AlfrescoAuthenticationService } from './alfresco-authentication.service
import { AlfrescoContentService } from './alfresco-content.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { StorageService } from './storage.service';
import { CookieService } from './cookie.service';
import { CookieServiceMock } from './../assets/cookie.service.mock';
import { LogService } from './log.service';
declare let jasmine: any;
@@ -42,6 +44,7 @@ describe('AlfrescoContentService', () => {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService
]);

View File

@@ -20,6 +20,8 @@ import { AlfrescoAuthenticationService } from './alfresco-authentication.service
import { AlfrescoApiService } from './alfresco-api.service';
import { StorageService } from './storage.service';
import { LogService } from './log.service';
import { CookieService } from './cookie.service';
import { CookieServiceMock } from './../assets/cookie.service.mock';
import { AuthGuardBpm } from './auth-guard-bpm.service';
import { Router} from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
@@ -34,6 +36,7 @@ describe('AuthGuardService BPM', () => {
AlfrescoApiService,
AlfrescoAuthenticationService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService],
imports: [RouterTestingModule]
});

View File

@@ -19,6 +19,8 @@ import { AlfrescoSettingsService } from './alfresco-settings.service';
import { AlfrescoAuthenticationService } from './alfresco-authentication.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { StorageService } from './storage.service';
import { CookieService } from './cookie.service';
import { CookieServiceMock } from './../assets/cookie.service.mock';
import { LogService } from './log.service';
import { AuthGuardEcm } from './auth-guard-ecm.service';
import { Router} from '@angular/router';
@@ -34,6 +36,7 @@ describe('AuthGuardService ECM', () => {
AlfrescoApiService,
AlfrescoAuthenticationService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService],
imports: [RouterTestingModule]
});

View File

@@ -19,6 +19,8 @@ import { AlfrescoSettingsService } from './alfresco-settings.service';
import { AlfrescoAuthenticationService } from './alfresco-authentication.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { StorageService } from './storage.service';
import { CookieService } from './cookie.service';
import { CookieServiceMock } from './../assets/cookie.service.mock';
import { LogService } from './log.service';
import { AuthGuard } from './auth-guard.service';
import { Router} from '@angular/router';
@@ -34,6 +36,7 @@ describe('AuthGuardService', () => {
AlfrescoApiService,
AlfrescoAuthenticationService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService],
imports: [RouterTestingModule]
});

View File

@@ -0,0 +1,48 @@
/*!
* @license
* Copyright 2016 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 { Injectable } from '@angular/core';
@Injectable()
export class CookieService {
/**
* Retrieve cookie by key.
*
* @returns {string | null}
*/
getItem(key: string): string | null {
const regexp = new RegExp('(?:' + key + '|;\s*' + key + ')=(.*?)(?:;|$)', 'g');
const result = regexp.exec(document.cookie);
return (result === null) ? null : result[1];
}
/**
* Set a cookie.
* @param key
* @param data
* @param expiration
* @param path
*
* @returns {boolean}
*/
setItem(key: string, data: string, expiration: Date | null, path: string | null): void {
document.cookie = `${key}=${data}` +
(expiration ? ';expires=' + expiration.toUTCString() : '') +
(path ? `;path=${path}` : ';path=/');
}
}

View File

@@ -17,6 +17,7 @@
export * from './content.service';
export * from './storage.service';
export * from './cookie.service';
export * from './alfresco-api.service';
export * from './alfresco-settings.service';
export * from './alfresco-content.service';

View File

@@ -15,8 +15,18 @@
* limitations under the License.
*/
import { AlfrescoSettingsService, AlfrescoAuthenticationService, AlfrescoApiService, StorageService, AlfrescoContentService, LogService, LogServiceMock } from 'ng2-alfresco-core';
import {
AlfrescoSettingsService,
AlfrescoAuthenticationService,
AlfrescoApiService,
StorageService,
CookieService,
AlfrescoContentService,
LogService,
LogServiceMock
} from 'ng2-alfresco-core';
import { FileNode } from '../assets/document-library.model.mock';
import { CookieServiceMock } from '../../../ng2-alfresco-core/src/assets/cookie.service.mock';
import { ReflectiveInjector } from '@angular/core';
import { DocumentListService } from './document-list.service';
@@ -100,6 +110,7 @@ describe('DocumentListService', () => {
AlfrescoContentService,
DocumentListService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
{ provide: LogService, useClass: LogServiceMock }
]);

View File

@@ -82,7 +82,7 @@
</button>
<div *ngIf="showRememberMe" class="adf-login__remember-me" id ="login-remember">
<md-checkbox class="rememberme-cb" checked="true">{{ 'LOGIN.LABEL.REMEMBER' | translate }}</md-checkbox>
<md-checkbox class="rememberme-cb" [checked]="rememberMe" (change)="rememberMe = !rememberMe">{{ 'LOGIN.LABEL.REMEMBER' | translate }}</md-checkbox>
</div>
</div>
<div class="mdl-card__actions mdl-card--border mdl-card__link">

View File

@@ -22,7 +22,7 @@ import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { AlfrescoLoginComponent } from './alfresco-login.component';
import { AuthenticationMock } from './../assets/authentication.service.mock';
import { TranslationMock } from './../assets/translation.service.mock';
import { MdInputModule } from '@angular/material';
import { MdInputModule, MdCheckboxModule } from '@angular/material';
describe('AlfrescoLogin', () => {
let component: AlfrescoLoginComponent;
@@ -39,6 +39,7 @@ describe('AlfrescoLogin', () => {
TestBed.configureTestingModule({
imports: [
MdInputModule,
MdCheckboxModule,
CoreModule.forRoot()
],
declarations: [AlfrescoLoginComponent],
@@ -64,11 +65,6 @@ describe('AlfrescoLogin', () => {
fixture.detectChanges();
});
describe('Login button', () => {
const getLoginButton = () => element.querySelector('#login-button');
const getLoginButtonText = () => element.querySelector('#login-button span.login-button-label').innerText;
function loginWithCredentials(username, password) {
component.providers = 'ECM';
usernameInput.value = username;
@@ -82,6 +78,11 @@ describe('AlfrescoLogin', () => {
fixture.detectChanges();
}
describe('Login button', () => {
const getLoginButton = () => element.querySelector('#login-button');
const getLoginButtonText = () => element.querySelector('#login-button span.login-button-label').innerText;
it('should be rendered with the proper key by default', () => {
expect(getLoginButton()).not.toBeNull();
expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.LOGIN');
@@ -110,6 +111,35 @@ describe('AlfrescoLogin', () => {
});
});
describe('Remember me', () => {
it('should be checked by default', () => {
expect(element.querySelector('.rememberme-cb input[type="checkbox"]').checked).toBe(true);
});
it('should set the component\'s rememberMe property properly', () => {
element.querySelector('.rememberme-cb').dispatchEvent(new Event('change'));
fixture.detectChanges();
expect(component.rememberMe).toBe(false);
element.querySelector('.rememberme-cb').dispatchEvent(new Event('change'));
fixture.detectChanges();
expect(component.rememberMe).toBe(true);
});
it('should be taken into consideration during login attempt', () => {
const authService = TestBed.get(AlfrescoAuthenticationService);
spyOn(authService, 'login').and.returnValue({ subscribe: () => { } });
component.rememberMe = false;
loginWithCredentials('fake-username', 'fake-password');
expect(authService.login).toHaveBeenCalledWith('fake-username', 'fake-password', false);
});
});
it('should render Login form with all the keys to be translated', () => {
expect(element.querySelector('[for="username"]')).toBeDefined();
expect(element.querySelector('[for="username"]').innerText).toEqual('LOGIN.LABEL.USERNAME');

View File

@@ -81,6 +81,7 @@ export class AlfrescoLoginComponent implements OnInit {
success: boolean = false;
actualLoginStep: any = LoginSteps.Landing;
LoginSteps: any = LoginSteps;
rememberMe: boolean = true;
formError: { [id: string]: string };
minLength: number = 2;
footerTemplate: TemplateRef<any>;
@@ -172,7 +173,7 @@ export class AlfrescoLoginComponent implements OnInit {
*/
private performLogin(values: any) {
this.actualLoginStep = LoginSteps.Checking;
this.authService.login(values.username, values.password)
this.authService.login(values.username, values.password, this.rememberMe)
.subscribe(
(token: any) => {
this.actualLoginStep = LoginSteps.Welcome;

View File

@@ -29,8 +29,10 @@ import {
AlfrescoTranslationService,
CoreModule,
StorageService,
CookieService,
LogService
} from 'ng2-alfresco-core';
import { CookieServiceMock } from './../../../ng2-alfresco-core/src/assets/cookie.service.mock';
import { DocumentListModule } from 'ng2-alfresco-documentlist';
describe('AlfrescoSearchComponent', () => {
@@ -148,6 +150,7 @@ describe('AlfrescoSearchComponent', () => {
AlfrescoSettingsService,
AlfrescoApiService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService,
{provide: ActivatedRoute, useValue: {params: Observable.from([{}])}}
]);

View File

@@ -17,7 +17,15 @@
import { ReflectiveInjector } from '@angular/core';
import { AlfrescoSearchService } from './alfresco-search.service';
import { AlfrescoAuthenticationService, AlfrescoSettingsService, AlfrescoApiService, StorageService, LogService } from 'ng2-alfresco-core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
StorageService,
CookieService,
LogService
} from 'ng2-alfresco-core';
import { CookieServiceMock } from './../../../ng2-alfresco-core/src/assets/cookie.service.mock';
import { fakeApi, fakeSearch, fakeError } from '../assets/alfresco-search.service.mock';
declare let jasmine: any;
@@ -35,6 +43,7 @@ describe('AlfrescoSearchService', () => {
AlfrescoApiService,
AlfrescoAuthenticationService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService
]);
service = injector.get(AlfrescoSearchService);

View File

@@ -17,7 +17,16 @@
import { ReflectiveInjector } from '@angular/core';
import { AlfrescoThumbnailService } from './alfresco-thumbnail.service';
import { AlfrescoApiService, AlfrescoAuthenticationService, AlfrescoContentService, AlfrescoSettingsService, StorageService, LogService } from 'ng2-alfresco-core';
import {
AlfrescoApiService,
AlfrescoAuthenticationService,
AlfrescoContentService,
AlfrescoSettingsService,
StorageService,
CookieService,
LogService
} from 'ng2-alfresco-core';
import { CookieServiceMock } from './../../../ng2-alfresco-core/src/assets/cookie.service.mock';
describe('AlfrescoThumbnailService', () => {
@@ -32,6 +41,7 @@ describe('AlfrescoThumbnailService', () => {
AlfrescoSettingsService,
AlfrescoThumbnailService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService
]);

View File

@@ -24,8 +24,10 @@ import {
AlfrescoSettingsService,
AlfrescoApiService,
StorageService,
CookieService,
LogService
} from 'ng2-alfresco-core';
import { CookieServiceMock } from './../../../ng2-alfresco-core/src/assets/cookie.service.mock';
declare let jasmine: any;
describe('EcmUserService', () => {
@@ -43,6 +45,7 @@ describe('EcmUserService', () => {
AlfrescoContentService,
EcmUserService,
StorageService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService
]);
});

View File

@@ -108,6 +108,7 @@
"istanbul-instrumenter-loader": "0.2.0",
"jasmine-ajax": "^3.2.0",
"jasmine-core": "2.4.1",
"happypack": "3.0.0",
"karma": "^0.13.22",
"karma-chrome-launcher": "~1.0.1",
"karma-coverage": "^1.1.1",