From 95e5316f34bfc780158eedf77cf88bf83d0e89ad Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Thu, 30 Jan 2020 17:16:18 +0100 Subject: [PATCH 1/8] WIP on Login --- projects/grange-core/src/lib/interfaces.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/grange-core/src/lib/interfaces.ts b/projects/grange-core/src/lib/interfaces.ts index b4810a0..5ff2fb1 100644 --- a/projects/grange-core/src/lib/interfaces.ts +++ b/projects/grange-core/src/lib/interfaces.ts @@ -10,6 +10,7 @@ export interface AuthenticatedStatus { export interface LoginInfo { login: string; password: string; + token: string; } export interface PasswordResetInfo { From cc31e5a9fbe07289065b908fe3c157608509e4c5 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Sat, 8 Feb 2020 14:03:56 +0100 Subject: [PATCH 2/8] Extending API and models --- .../src/lib/authentication.service.ts | 463 ++++++++++-------- projects/grange-core/src/lib/interfaces.ts | 4 + 2 files changed, 250 insertions(+), 217 deletions(-) diff --git a/projects/grange-core/src/lib/authentication.service.ts b/projects/grange-core/src/lib/authentication.service.ts index 384df21..76e059e 100644 --- a/projects/grange-core/src/lib/authentication.service.ts +++ b/projects/grange-core/src/lib/authentication.service.ts @@ -1,7 +1,7 @@ import { - HttpClient, - HttpErrorResponse, - HttpHeaders, + HttpClient, + HttpErrorResponse, + HttpHeaders, } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -10,245 +10,274 @@ import { tap, catchError, map } from 'rxjs/operators'; import { ConfigurationService } from './configuration.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class AuthenticationService { - isAuthenticated: BehaviorSubject = new BehaviorSubject( - { state: false, pending: false, username: null }, + isAuthenticated: BehaviorSubject = new BehaviorSubject( + { state: false, pending: false, username: null }, + ); + basicCredentials?: string[]; + + constructor( + protected config: ConfigurationService, + protected http: HttpClient, + ) { + let token = localStorage.getItem('auth'); + const lastLogin = localStorage.getItem('auth_time'); + // token expires after 12 hours + const expire = config.get( + 'AUTH_TOKEN_EXPIRES', + 12 * 60 * 60 * 1000, ); - basicCredentials?: string[]; - - constructor( - protected config: ConfigurationService, - protected http: HttpClient, - ) { - let token = localStorage.getItem('auth'); - const lastLogin = localStorage.getItem('auth_time'); - // token expires after 12 hours - const expire = config.get( - 'AUTH_TOKEN_EXPIRES', - 12 * 60 * 60 * 1000, - ); - if (!lastLogin || Date.now() - Date.parse(lastLogin) > expire) { - localStorage.removeItem('auth'); - token = null; - } - if (token) { - this.isAuthenticated.next({ - state: true, - pending: false, - username: this.getUsername(), - }); - } + if (!lastLogin || Date.now() - Date.parse(lastLogin) > expire) { + localStorage.removeItem('auth'); + token = null; } - - getUsername(): string | null { - const userTokenInfo = this.getUserTokenInfo(); - if (userTokenInfo === null) { - return null; - } else { - return userTokenInfo.username || userTokenInfo.sub || userTokenInfo.id || null; - } + if (token) { + this.isAuthenticated.next({ + state: true, + pending: false, + username: this.getUsername(), + }); } + } - protected getUserTokenInfo(): UserInfoTokenParts | null { - const token = localStorage.getItem('auth'); - if (token) { - const tokenParts = token.split('.'); - return JSON.parse(atob(tokenParts[1])) as UserInfoTokenParts; - } else { - return null; - } + getUsername(): string | null { + const userTokenInfo = this.getUserTokenInfo(); + if (userTokenInfo === null) { + return null; + } else { + return userTokenInfo.username || userTokenInfo.sub || userTokenInfo.id || null; } + } - setBasicCredentials(login: string, password: string, temporary = false) { - this.basicCredentials = [login, password]; - this.isAuthenticated.next({ - state: !temporary, - pending: temporary, - username: login, - }); + protected getUserTokenInfo(): UserInfoTokenParts | null { + const token = localStorage.getItem('auth'); + if (token) { + const tokenParts = token.split('.'); + return JSON.parse(atob(tokenParts[1])) as UserInfoTokenParts; + } else { + return null; } + } - cleanBasicCredentials() { - delete this.basicCredentials; - this.isAuthenticated.next({ - state: false, - pending: false, - username: null, - }); - } + setBasicCredentials(login: string, password: string, temporary = false) { + this.basicCredentials = [login, password]; + this.isAuthenticated.next({ + state: !temporary, + pending: temporary, + username: login, + }); + } - setAuthToken(token: string) { - localStorage.setItem('auth', token); - localStorage.setItem( - 'auth_time', - new Date().toISOString(), - ); - this.isAuthenticated.next({ - state: true, - pending: false, - username: this.getUsername(), - }); - } + cleanBasicCredentials() { + delete this.basicCredentials; + this.isAuthenticated.next({ + state: false, + pending: false, + username: null, + }); + } - login(login: string, password: string, path?: string): Observable { - const headers = this.getHeaders(); - const body = JSON.stringify({ - login, - password, - }); - return this.http - .post(this.config.get('BACKEND_URL') + (path || '') + '/@login', body, { - headers, - }).pipe( - map((data: LoginToken) => { - if (data.token) { - this.setAuthToken(data.token); - return true; - } else { - localStorage.removeItem('auth'); - localStorage.removeItem('auth_time'); - this.isAuthenticated.next({ - state: false, - pending: false, - username: null, - }); - return false; - } - }), - catchError((errorResponse: HttpErrorResponse) => { - const error = getError(errorResponse); - if (errorResponse.status === 404) { - // @login endpoint does not exist on this backend - // we keep with basic auth - this.setBasicCredentials(login, password, false); - } else { - localStorage.removeItem('auth'); - localStorage.removeItem('auth_time'); - this.isAuthenticated.next({ - state: false, - pending: false, - username: null, - error: error.message, - }); - } - return Observable.throw(error); - }) - ); - } + setAuthToken(token: string) { + localStorage.setItem('auth', token); + localStorage.setItem( + 'auth_time', + new Date().toISOString(), + ); + this.isAuthenticated.next({ + state: true, + pending: false, + username: this.getUsername(), + }); + } - logout() { - this.cleanBasicCredentials(); - localStorage.removeItem('auth'); - localStorage.removeItem('auth_time'); - this.isAuthenticated.next({ state: false, pending: false, username: null }); - } + login(login: string, password: string, path?: string): Observable { + const headers = this.getHeaders(); + const body = JSON.stringify({ + login, + password, + }); + return this.http + .post(this.config.get('BACKEND_URL') + (path || '') + '/@login', body, { + headers, + }).pipe( + map((data: LoginToken) => { + if (data.token) { + this.setAuthToken(data.token); + return true; + } else { + localStorage.removeItem('auth'); + localStorage.removeItem('auth_time'); + this.isAuthenticated.next({ + state: false, + pending: false, + username: null, + }); + return false; + } + }), + catchError((errorResponse: HttpErrorResponse) => { + const error = getError(errorResponse); + if (errorResponse.status === 404) { + // @login endpoint does not exist on this backend + // we keep with basic auth + this.setBasicCredentials(login, password, false); + } else { + localStorage.removeItem('auth'); + localStorage.removeItem('auth_time'); + this.isAuthenticated.next({ + state: false, + pending: false, + username: null, + error: error.message, + }); + } + return Observable.throw(error); + }) + ); + } - requestPasswordReset(login: string): Observable { - const headers = this.getHeaders(); - const url = - this.config.get('BACKEND_URL') + `/@users/${login}/reset-password`; - return this.http - .post(url, {}, { headers: headers }) - .pipe( - catchError(this.error.bind(this)) - ); - } + logout() { + this.cleanBasicCredentials(); + localStorage.removeItem('auth'); + localStorage.removeItem('auth_time'); + this.isAuthenticated.next({ state: false, pending: false, username: null }); + } - passwordReset(resetInfo: PasswordResetInfo): Observable { - const headers = this.getHeaders(); - const data: { [key: string]: string } = { - new_password: resetInfo.newPassword, - }; - if (resetInfo.oldPassword) { - data['old_password'] = resetInfo.oldPassword; - } - if (resetInfo.token) { - data['reset_token'] = resetInfo.token; - } - const url = - this.config.get('BACKEND_URL') + - `/@users/${resetInfo.login}/reset-password`; - return this.http - .post(url, data, { headers: headers }) - .pipe( - catchError(this.error.bind(this)) - ); - } + requestPasswordReset(login: string): Observable { + const headers = this.getHeaders(); + const url = + this.config.get('BACKEND_URL') + `/@users/${login}/reset-password`; + return this.http + .post(url, {}, { headers: headers }) + .pipe( + catchError(this.error.bind(this)) + ); + } - getHeaders(): HttpHeaders { - let headers = new HttpHeaders(); - headers = headers.set('Accept', 'application/json'); - headers = headers.set('Content-Type', 'application/json'); - const auth = localStorage.getItem('auth'); - if (auth) { - headers = headers.set('Authorization', 'Bearer ' + auth); - } else if (!!this.basicCredentials) { - headers = headers.set('Authorization', 'Basic ' + btoa(this.basicCredentials.join(':'))); - } - return headers; + passwordReset(resetInfo: PasswordResetInfo): Observable { + const headers = this.getHeaders(); + const data: { [key: string]: string } = { + new_password: resetInfo.newPassword, + }; + if (resetInfo.oldPassword) { + data['old_password'] = resetInfo.oldPassword; } - - setAuthenticated(isAuthenticated: boolean) { - this.isAuthenticated.next({ state: isAuthenticated, pending: false, username: this.getUsername() }); + if (resetInfo.token) { + data['reset_token'] = resetInfo.token; } + const url = + this.config.get('BACKEND_URL') + + `/@users/${resetInfo.login}/reset-password`; + return this.http + .post(url, data, { headers: headers }) + .pipe( + catchError(this.error.bind(this)) + ); + } - protected error(errorResponse: HttpErrorResponse): Observable { - const error: Error = getError(errorResponse); - return Observable.throw(error); + getHeaders(): HttpHeaders { + let headers = new HttpHeaders(); + headers = headers.set('Accept', 'application/json'); + headers = headers.set('Content-Type', 'application/json'); + const auth = localStorage.getItem('auth'); + if (auth) { + headers = headers.set('Authorization', 'Bearer ' + auth); + } else if (!!this.basicCredentials) { + headers = headers.set('Authorization', 'Basic ' + btoa(this.basicCredentials.join(':'))); } + return headers; + } + + setAuthenticated(isAuthenticated: boolean) { + this.isAuthenticated.next({ state: isAuthenticated, pending: false, username: this.getUsername() }); + } + + protected error(errorResponse: HttpErrorResponse): Observable { + const error: Error = getError(errorResponse); + return Observable.throw(error); + } + + goSocialLogin(provider, callback) { + const callUrl = `${this.config.get('BACKEND_URL')}/@authenticate/${provider}?callback=${location.origin}/${callback}${provider}`; + window.location.href = callUrl; + } + + getValidationSchema(token) { + const headers = this.getHeaders(); + const url = + this.config.get('BACKEND_URL') + + `/@validate_schema/${token}`; + return this.http + .get(url, { headers: headers }) + .pipe( + catchError(this.error.bind(this)) + ); + } + + doValidation(token, model) { + const headers = this.getHeaders(); + const url = + this.config.get('BACKEND_URL') + + `/@validation/${token}`; + return this.http + .post(url, model, { headers: headers }) + .pipe( + catchError(this.error.bind(this)) + ); + } } export function getError(errorResponse: HttpErrorResponse): Error { - let error: Error; - if (errorResponse.error) { - let errorResponseError: any = errorResponse.error; - try { - // string plone error - errorResponseError = JSON.parse(errorResponseError); - if (errorResponseError.error && errorResponseError.error.message) { - // two levels of error properties - error = Object.assign({}, errorResponseError.error); - } else { - error = errorResponseError; - } - } catch (SyntaxError) { - if (errorResponseError.message && errorResponseError.type) { - // object plone error - error = errorResponseError; - } else if ( - typeof errorResponseError.error === 'object' && - errorResponseError.error.type - ) { - // object plone error with two levels of error properties - error = Object.assign({}, errorResponseError.error); - } else { - // not a plone error - error = { - type: errorResponse.statusText, - message: errorResponse.message, - traceback: [], - }; - } - } - } else { + let error: Error; + if (errorResponse.error) { + let errorResponseError: any = errorResponse.error; + try { + // string plone error + errorResponseError = JSON.parse(errorResponseError); + if (errorResponseError.error && errorResponseError.error.message) { + // two levels of error properties + error = Object.assign({}, errorResponseError.error); + } else { + error = errorResponseError; + } + } catch (SyntaxError) { + if (errorResponseError.message && errorResponseError.type) { + // object plone error + error = errorResponseError; + } else if ( + typeof errorResponseError.error === 'object' && + errorResponseError.error.type + ) { + // object plone error with two levels of error properties + error = Object.assign({}, errorResponseError.error); + } else { + // not a plone error error = { - type: errorResponse.statusText, - message: errorResponse.message, - traceback: [], + type: errorResponse.statusText, + message: errorResponse.message, + traceback: [], }; + } } - // check if message is a jsonified list - try { - const parsedMessage = JSON.parse(error.message); - if (Array.isArray(parsedMessage)) { // a list of errors - dexterity validation error for instance - error.errors = parsedMessage; - error.message = errorResponse.message; - } - } catch (SyntaxError) { - // + } else { + error = { + type: errorResponse.statusText, + message: errorResponse.message, + traceback: [], + }; + } + // check if message is a jsonified list + try { + const parsedMessage = JSON.parse(error.message); + if (Array.isArray(parsedMessage)) { // a list of errors - dexterity validation error for instance + error.errors = parsedMessage; + error.message = errorResponse.message; } - error.response = errorResponse; - return error; + } catch (SyntaxError) { + // + } + error.response = errorResponse; + return error; } diff --git a/projects/grange-core/src/lib/interfaces.ts b/projects/grange-core/src/lib/interfaces.ts index 5ff2fb1..07cd4fb 100644 --- a/projects/grange-core/src/lib/interfaces.ts +++ b/projects/grange-core/src/lib/interfaces.ts @@ -13,6 +13,10 @@ export interface LoginInfo { token: string; } +export interface RecoverInfo { + login: string; +} + export interface PasswordResetInfo { oldPassword?: string; newPassword: string; From 51a1a81f3de3eb5a3eb4bd9fedf5d84a1027f472 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Sat, 8 Feb 2020 17:13:54 +0100 Subject: [PATCH 3/8] Fix --- projects/grange-core/src/lib/authentication.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/grange-core/src/lib/authentication.service.ts b/projects/grange-core/src/lib/authentication.service.ts index 76e059e..8f99f01 100644 --- a/projects/grange-core/src/lib/authentication.service.ts +++ b/projects/grange-core/src/lib/authentication.service.ts @@ -204,7 +204,7 @@ export class AuthenticationService { window.location.href = callUrl; } - getValidationSchema(token) { + getValidationSchema(token: string): Observable { const headers = this.getHeaders(); const url = this.config.get('BACKEND_URL') + @@ -216,11 +216,11 @@ export class AuthenticationService { ); } - doValidation(token, model) { + doValidation(token: string, model: any): Observable { const headers = this.getHeaders(); const url = this.config.get('BACKEND_URL') + - `/@validation/${token}`; + `/@validate/${token}`; return this.http .post(url, model, { headers: headers }) .pipe( From d7081f067bc43c1e324fcfc400d962674f8b1011 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Mon, 10 Feb 2020 20:12:58 +0100 Subject: [PATCH 4/8] Fixing post --- projects/grange-core/src/lib/authentication.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/grange-core/src/lib/authentication.service.ts b/projects/grange-core/src/lib/authentication.service.ts index 8f99f01..6443c4b 100644 --- a/projects/grange-core/src/lib/authentication.service.ts +++ b/projects/grange-core/src/lib/authentication.service.ts @@ -210,7 +210,7 @@ export class AuthenticationService { this.config.get('BACKEND_URL') + `/@validate_schema/${token}`; return this.http - .get(url, { headers: headers }) + .post(url, {}, { headers: headers }) .pipe( catchError(this.error.bind(this)) ); From a0ffcaace61eb29f5add11515eea3ff63bb1cc39 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Tue, 11 Feb 2020 12:02:06 +0100 Subject: [PATCH 5/8] Recaptcha service --- package.json | 3 +- .../src/lib/authentication.service.ts | 179 +++++++++++++++--- projects/grange-core/src/lib/interfaces.ts | 6 + .../src/lib/recaptcha_v3.service.ts | 119 ++++++++++++ 4 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 projects/grange-core/src/lib/recaptcha_v3.service.ts diff --git a/package.json b/package.json index fa2fa03..843215b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "@briebug/jest-schematic": "^2.1.0", "rxjs": "~6.4.0", "tslib": "^1.10.0", - "zone.js": "~0.9.1" + "zone.js": "~0.9.1", + "ngx-captcha": "^7.0.0" }, "devDependencies": { "@angular-devkit/build-angular": "~0.803.9", diff --git a/projects/grange-core/src/lib/authentication.service.ts b/projects/grange-core/src/lib/authentication.service.ts index 6443c4b..cfd574f 100644 --- a/projects/grange-core/src/lib/authentication.service.ts +++ b/projects/grange-core/src/lib/authentication.service.ts @@ -7,6 +7,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { AuthenticatedStatus, Error, PasswordResetInfo, UserInfoTokenParts, LoginToken } from './interfaces'; import { tap, catchError, map } from 'rxjs/operators'; +import { ReCaptchaV3Service } from './recaptcha_v3.service'; import { ConfigurationService } from './configuration.service'; @Injectable({ @@ -21,6 +22,7 @@ export class AuthenticationService { constructor( protected config: ConfigurationService, protected http: HttpClient, + protected recaptcha: ReCaptchaV3Service ) { let token = localStorage.getItem('auth'); const lastLogin = localStorage.getItem('auth_time'); @@ -92,8 +94,45 @@ export class AuthenticationService { }); } + getHeaders(): HttpHeaders { + let headers = new HttpHeaders(); + headers = headers.set('Accept', 'application/json'); + headers = headers.set('Content-Type', 'application/json'); + const auth = localStorage.getItem('auth'); + if (auth) { + headers = headers.set('Authorization', 'Bearer ' + auth); + } else if (!!this.basicCredentials) { + headers = headers.set('Authorization', 'Basic ' + btoa(this.basicCredentials.join(':'))); + } + return headers; + } + + setAuthenticated(isAuthenticated: boolean): void { + this.isAuthenticated.next({ state: isAuthenticated, pending: false, username: this.getUsername() }); + } + + protected error(errorResponse: HttpErrorResponse): Observable { + const error: Error = getError(errorResponse); + return Observable.throw(error); + } + + // LOGIN LOGIC + login(login: string, password: string, path?: string): Observable { + if (this.config.get('RECAPTCHA_TOKEN')) { + this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'login', (token) => { + return this.doLogin(login, password, path, token); + }); + } else { + return this.doLogin(login, password, path); + } + } + + doLogin(login: string, password: string, path?: string, recaptcha?: string): Observable { const headers = this.getHeaders(); + if (recaptcha) { + headers['X-VALIDATION-G'] = recaptcha; + } const body = JSON.stringify({ login, password, @@ -138,15 +177,54 @@ export class AuthenticationService { ); } - logout() { + // LOGOUT LOGIC + + private _logout() { this.cleanBasicCredentials(); localStorage.removeItem('auth'); localStorage.removeItem('auth_time'); this.isAuthenticated.next({ state: false, pending: false, username: null }); } - requestPasswordReset(login: string): Observable { + doLogout(recaptcha?: string) { + const headers = this.getHeaders(); + if (recaptcha !== undefined) { + headers['X-VALIDATION-G'] = recaptcha; + } + const url = + this.config.get('BACKEND_URL') + `/@logout`; + this.http + .post(url, {}, { headers: headers }) + .pipe( + catchError(this.error.bind(this)) + ).subscribe( + res => { + this._logout(); + }, + err => { + this._logout(); + } + ); + + } + + logout() { + if (this.config.get('RECAPTCHA_TOKEN')) { + this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'logout', (token) => { + this.doLogout(token); + }); + } else { + this.doLogout(); + } + } + + // RESET PASSWORD LOGIN + + doRequestPasswordReset(login: string, recaptcha?: string): Observable { const headers = this.getHeaders(); + if (recaptcha !== undefined) { + headers['X-VALIDATION-G'] = recaptcha; + } const url = this.config.get('BACKEND_URL') + `/@users/${login}/reset-password`; return this.http @@ -156,6 +234,18 @@ export class AuthenticationService { ); } + requestPasswordReset(login: string): Observable { + if (this.config.get('RECAPTCHA_TOKEN')) { + this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'reset', (token) => { + return this.doRequestPasswordReset(login, token); + }); + } else { + return this.doRequestPasswordReset(login); + } + } + + // CHANGE PASSWORD + passwordReset(resetInfo: PasswordResetInfo): Observable { const headers = this.getHeaders(); const data: { [key: string]: string } = { @@ -177,35 +267,18 @@ export class AuthenticationService { ); } - getHeaders(): HttpHeaders { - let headers = new HttpHeaders(); - headers = headers.set('Accept', 'application/json'); - headers = headers.set('Content-Type', 'application/json'); - const auth = localStorage.getItem('auth'); - if (auth) { - headers = headers.set('Authorization', 'Bearer ' + auth); - } else if (!!this.basicCredentials) { - headers = headers.set('Authorization', 'Basic ' + btoa(this.basicCredentials.join(':'))); - } - return headers; - } - - setAuthenticated(isAuthenticated: boolean) { - this.isAuthenticated.next({ state: isAuthenticated, pending: false, username: this.getUsername() }); - } - - protected error(errorResponse: HttpErrorResponse): Observable { - const error: Error = getError(errorResponse); - return Observable.throw(error); - } - - goSocialLogin(provider, callback) { + goSocialLogin(provider, callback): void { const callUrl = `${this.config.get('BACKEND_URL')}/@authenticate/${provider}?callback=${location.origin}/${callback}${provider}`; window.location.href = callUrl; } - getValidationSchema(token: string): Observable { + // VALIDATION LOGIC + + doGetValidationSchema(token: string, recaptcha?: string): Observable { const headers = this.getHeaders(); + if (recaptcha !== undefined) { + headers['X-VALIDATION-G'] = recaptcha; + } const url = this.config.get('BACKEND_URL') + `/@validate_schema/${token}`; @@ -216,17 +289,65 @@ export class AuthenticationService { ); } - doValidation(token: string, model: any): Observable { + getValidationSchema(token: string): Observable { + if (this.config.get('RECAPTCHA_TOKEN')) { + this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'schema', (recaptcha) => { + return this.doGetValidationSchema(token, recaptcha); + }); + } else { + return this.doGetValidationSchema(token); + } + } + + doRealValidation(token: string, model: any, recaptcha?: string): Observable { const headers = this.getHeaders(); + if (recaptcha !== undefined) { + headers['X-VALIDATION-G'] = recaptcha; + } const url = - this.config.get('BACKEND_URL') + - `/@validate/${token}`; + this.config.get('BACKEND_URL') + + `/@validate/${token}`; return this.http .post(url, model, { headers: headers }) .pipe( catchError(this.error.bind(this)) ); } + + + doValidation(token: string, model: any): Observable { + if (this.config.get('RECAPTCHA_TOKEN')) { + this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'validation', (recaptcha) => { + return this.doRealValidation(token, model, recaptcha); + }); + } else { + return this.doRealValidation(token, model); + } + } + + doGetInfo(recaptcha?: string) { + const headers = this.getHeaders(); + if (recaptcha !== undefined) { + headers['X-VALIDATION-G'] = recaptcha; + } + const url = + this.config.get('BACKEND_URL') + `/@info`; + return this.http + .get(url, { headers: headers }) + .pipe( + catchError(this.error.bind(this)) + ); + } + + getInfo(): Observable { + if (this.config.get('RECAPTCHA_TOKEN')) { + this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'info', (recaptcha) => { + return this.doGetInfo(recaptcha); + }); + } else { + return this.doGetInfo(); + } + } } export function getError(errorResponse: HttpErrorResponse): Error { diff --git a/projects/grange-core/src/lib/interfaces.ts b/projects/grange-core/src/lib/interfaces.ts index 07cd4fb..e256095 100644 --- a/projects/grange-core/src/lib/interfaces.ts +++ b/projects/grange-core/src/lib/interfaces.ts @@ -17,6 +17,12 @@ export interface RecoverInfo { login: string; } +export interface ContainerInfo { + register: boolean; + social: string[]; +} + + export interface PasswordResetInfo { oldPassword?: string; newPassword: string; diff --git a/projects/grange-core/src/lib/recaptcha_v3.service.ts b/projects/grange-core/src/lib/recaptcha_v3.service.ts new file mode 100644 index 0000000..134b2e5 --- /dev/null +++ b/projects/grange-core/src/lib/recaptcha_v3.service.ts @@ -0,0 +1,119 @@ +// Code refactored from ngx-captcha package because was not released with promise call +import { Injectable, Inject, NgZone } from '@angular/core'; + +@Injectable() +export class ReCaptchaV3Service { + + protected readonly windowGrecaptcha = 'grecaptcha'; + + protected readonly windowOnLoadCallbackProperty = 'ngx_captcha_onload_callback'; + + protected readonly globalDomain: string = 'recaptcha.net'; + + protected readonly defaultDomain: string = 'google.com'; + + constructor( + protected zone: NgZone, + @Inject('LANG') protected lang: any, + ) { + } + + registerCaptchaScript(useGlobalDomain: boolean, render: string, onLoad: (grecaptcha: any) => void, language?: string): void { + if (this.grecaptchaScriptLoaded()) { + // recaptcha script is already loaded + // just call the callback + this.zone.run(() => { + onLoad(window[this.windowGrecaptcha]); + }); + return; + } + + // we need to patch the callback through global variable, otherwise callback is not accessible + // note: https://github.com/Enngage/ngx-captcha/issues/2 + window[this.windowOnLoadCallbackProperty] = (() => this.zone.run( + onLoad.bind(this, window[this.windowGrecaptcha]) + )); + + // prepare script elem + const scriptElem = document.createElement('script'); + scriptElem.innerHTML = ''; + scriptElem.src = this.getCaptchaScriptUrl(useGlobalDomain, render, language); + scriptElem.async = true; + scriptElem.defer = true; + + // add script to header + document.getElementsByTagName('head')[0].appendChild(scriptElem); + } + + cleanup(): void { + window[this.windowOnLoadCallbackProperty] = undefined; + window[this.windowGrecaptcha] = undefined; + } + + private grecaptchaScriptLoaded(): boolean { + if (window[this.windowOnLoadCallbackProperty] && window[this.windowGrecaptcha]) { + return true; + } + return false; + } + + private getLanguageParam(hl?: string): string { + if (!hl) { + return ''; + } + + return `&hl=${hl}`; + } + + private getCaptchaScriptUrl(useGlobalDomain: boolean, render: string, language?: string): string { + const domain = useGlobalDomain ? this.globalDomain : this.defaultDomain; + + // tslint:disable-next-line:max-line-length + return `https://www.${domain}/recaptcha/api.js?onload=${this.windowOnLoadCallbackProperty}&render=${render}${this.getLanguageParam(language)}`; + } + + executeAsPromise( + siteKey: string, + action: string, + config?: { + useGlobalDomain: boolean; + } + ): Promise { + return new Promise((resolve, reject) => { + const useGlobalDomain = config && config.useGlobalDomain ? true : false; + + const onRegister = grecaptcha => { + this.zone.runOutsideAngular(() => { + grecaptcha + .execute(siteKey, { + action: action, + }) + .then(token => { + this.zone.run(() => { + resolve(token); + }); + }) + .catch(reject); + }); + }; + + this.registerCaptchaScript( + useGlobalDomain, + siteKey, + onRegister, + this.lang + ); + }); + } + + execute( + siteKey: string, + action: string, + callback: (token: string) => void, + config?: { + useGlobalDomain: boolean; + } + ): void { + this.executeAsPromise(siteKey, action, config).then(callback); + } +} From c160302be262111508586d804ce6d9033281b628 Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Tue, 11 Feb 2020 14:10:45 +0100 Subject: [PATCH 6/8] Adding recaptcha --- projects/grange-core/src/index.ts | 1 + .../src/lib/authentication.service.ts | 106 +++++++++--------- .../src/lib/recaptcha_v3.service.ts | 5 +- 3 files changed, 57 insertions(+), 55 deletions(-) diff --git a/projects/grange-core/src/index.ts b/projects/grange-core/src/index.ts index 1d7fbd7..692c2e7 100644 --- a/projects/grange-core/src/index.ts +++ b/projects/grange-core/src/index.ts @@ -2,6 +2,7 @@ export { GrangeCore } from './lib/core.service'; export { APIService } from './lib/api.service'; export { AuthenticationService } from './lib/authentication.service'; export { CacheService } from './lib/cache.service'; +export { ReCaptchaV3Service } from './lib/recaptcha_v3.service'; export { ConfigurationService } from './lib/configuration.service'; export * from './lib/loading.service'; export * from './lib/resource.service'; diff --git a/projects/grange-core/src/lib/authentication.service.ts b/projects/grange-core/src/lib/authentication.service.ts index cfd574f..2cf9d05 100644 --- a/projects/grange-core/src/lib/authentication.service.ts +++ b/projects/grange-core/src/lib/authentication.service.ts @@ -4,9 +4,9 @@ import { HttpHeaders, } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, from } from 'rxjs'; import { AuthenticatedStatus, Error, PasswordResetInfo, UserInfoTokenParts, LoginToken } from './interfaces'; -import { tap, catchError, map } from 'rxjs/operators'; +import { tap, catchError, map, concatMap } from 'rxjs/operators'; import { ReCaptchaV3Service } from './recaptcha_v3.service'; import { ConfigurationService } from './configuration.service'; @@ -94,7 +94,7 @@ export class AuthenticationService { }); } - getHeaders(): HttpHeaders { + getHeaders(recaptcha?: string): HttpHeaders { let headers = new HttpHeaders(); headers = headers.set('Accept', 'application/json'); headers = headers.set('Content-Type', 'application/json'); @@ -104,6 +104,9 @@ export class AuthenticationService { } else if (!!this.basicCredentials) { headers = headers.set('Authorization', 'Basic ' + btoa(this.basicCredentials.join(':'))); } + if (recaptcha) { + headers = headers.set('X-VALIDATION-G', recaptcha); + } return headers; } @@ -120,19 +123,21 @@ export class AuthenticationService { login(login: string, password: string, path?: string): Observable { if (this.config.get('RECAPTCHA_TOKEN')) { - this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'login', (token) => { - return this.doLogin(login, password, path, token); - }); + const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'login'); + return from(promise).pipe( + concatMap((token: string) => { + return this.doLogin(login, password, path, token); + }), + catchError((err) => { + return Observable.throw(err); + })); } else { return this.doLogin(login, password, path); } } doLogin(login: string, password: string, path?: string, recaptcha?: string): Observable { - const headers = this.getHeaders(); - if (recaptcha) { - headers['X-VALIDATION-G'] = recaptcha; - } + const headers = this.getHeaders(recaptcha); const body = JSON.stringify({ login, password, @@ -186,11 +191,8 @@ export class AuthenticationService { this.isAuthenticated.next({ state: false, pending: false, username: null }); } - doLogout(recaptcha?: string) { + logout() { const headers = this.getHeaders(); - if (recaptcha !== undefined) { - headers['X-VALIDATION-G'] = recaptcha; - } const url = this.config.get('BACKEND_URL') + `/@logout`; this.http @@ -205,26 +207,12 @@ export class AuthenticationService { this._logout(); } ); - - } - - logout() { - if (this.config.get('RECAPTCHA_TOKEN')) { - this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'logout', (token) => { - this.doLogout(token); - }); - } else { - this.doLogout(); - } } // RESET PASSWORD LOGIN doRequestPasswordReset(login: string, recaptcha?: string): Observable { - const headers = this.getHeaders(); - if (recaptcha !== undefined) { - headers['X-VALIDATION-G'] = recaptcha; - } + const headers = this.getHeaders(recaptcha); const url = this.config.get('BACKEND_URL') + `/@users/${login}/reset-password`; return this.http @@ -236,9 +224,14 @@ export class AuthenticationService { requestPasswordReset(login: string): Observable { if (this.config.get('RECAPTCHA_TOKEN')) { - this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'reset', (token) => { - return this.doRequestPasswordReset(login, token); - }); + const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'reset'); + return from(promise).pipe( + concatMap((token: string) => { + return this.doRequestPasswordReset(login, token) + }), + catchError((err) => { + return Observable.throw(err); + })); } else { return this.doRequestPasswordReset(login); } @@ -275,10 +268,7 @@ export class AuthenticationService { // VALIDATION LOGIC doGetValidationSchema(token: string, recaptcha?: string): Observable { - const headers = this.getHeaders(); - if (recaptcha !== undefined) { - headers['X-VALIDATION-G'] = recaptcha; - } + const headers = this.getHeaders(recaptcha); const url = this.config.get('BACKEND_URL') + `/@validate_schema/${token}`; @@ -291,19 +281,21 @@ export class AuthenticationService { getValidationSchema(token: string): Observable { if (this.config.get('RECAPTCHA_TOKEN')) { - this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'schema', (recaptcha) => { - return this.doGetValidationSchema(token, recaptcha); - }); + const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'schema'); + return from(promise).pipe( + concatMap((recaptcha: string) => { + return this.doGetValidationSchema(token, recaptcha) + }), + catchError((err) => { + return Observable.throw(err); + })); } else { return this.doGetValidationSchema(token); } } doRealValidation(token: string, model: any, recaptcha?: string): Observable { - const headers = this.getHeaders(); - if (recaptcha !== undefined) { - headers['X-VALIDATION-G'] = recaptcha; - } + const headers = this.getHeaders(recaptcha); const url = this.config.get('BACKEND_URL') + `/@validate/${token}`; @@ -317,19 +309,21 @@ export class AuthenticationService { doValidation(token: string, model: any): Observable { if (this.config.get('RECAPTCHA_TOKEN')) { - this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'validation', (recaptcha) => { - return this.doRealValidation(token, model, recaptcha); - }); + const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'validation'); + return from(promise).pipe( + concatMap((recaptcha: string) => { + return this.doRealValidation(token, model, recaptcha) + }), + catchError((err) => { + return Observable.throw(err); + })); } else { return this.doRealValidation(token, model); } } doGetInfo(recaptcha?: string) { - const headers = this.getHeaders(); - if (recaptcha !== undefined) { - headers['X-VALIDATION-G'] = recaptcha; - } + const headers = this.getHeaders(recaptcha); const url = this.config.get('BACKEND_URL') + `/@info`; return this.http @@ -341,9 +335,15 @@ export class AuthenticationService { getInfo(): Observable { if (this.config.get('RECAPTCHA_TOKEN')) { - this.recaptcha.execute(this.config.get('RECAPTCHA_TOKEN'), 'info', (recaptcha) => { - return this.doGetInfo(recaptcha); - }); + + const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'info'); + return from(promise).pipe( + concatMap((recaptcha: string) => { + return this.doGetInfo(recaptcha); + }), + catchError((err) => { + return Observable.throw(err); + })); } else { return this.doGetInfo(); } diff --git a/projects/grange-core/src/lib/recaptcha_v3.service.ts b/projects/grange-core/src/lib/recaptcha_v3.service.ts index 134b2e5..049701a 100644 --- a/projects/grange-core/src/lib/recaptcha_v3.service.ts +++ b/projects/grange-core/src/lib/recaptcha_v3.service.ts @@ -1,7 +1,9 @@ // Code refactored from ngx-captcha package because was not released with promise call import { Injectable, Inject, NgZone } from '@angular/core'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class ReCaptchaV3Service { protected readonly windowGrecaptcha = 'grecaptcha'; @@ -93,7 +95,6 @@ export class ReCaptchaV3Service { resolve(token); }); }) - .catch(reject); }); }; From 38523af55047b09e0f60218d793bde05660140bf Mon Sep 17 00:00:00 2001 From: Ramon Navarro Bosch Date: Thu, 20 Feb 2020 12:39:00 +0100 Subject: [PATCH 7/8] No recaptcha needed --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 843215b..fa2fa03 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,7 @@ "@briebug/jest-schematic": "^2.1.0", "rxjs": "~6.4.0", "tslib": "^1.10.0", - "zone.js": "~0.9.1", - "ngx-captcha": "^7.0.0" + "zone.js": "~0.9.1" }, "devDependencies": { "@angular-devkit/build-angular": "~0.803.9", From 9993b19ef1e3e4ffc61459745fe3ea080f95fc47 Mon Sep 17 00:00:00 2001 From: Eric BREHAULT Date: Thu, 20 Feb 2020 21:09:51 +0100 Subject: [PATCH 8/8] version + cleanup --- CHANGELOG.md | 4 ++ projects/grange-core/package.json | 4 +- .../grange-core/src/lib/api.service.spec.ts | 3 +- .../src/lib/authentication.service.spec.ts | 11 ++++-- .../src/lib/authentication.service.ts | 38 +++++++++---------- .../grange-core/src/lib/cache.service.spec.ts | 3 ++ .../src/lib/resource.service.spec.ts | 8 ++-- 7 files changed, 41 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d30d60a..6659705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.1.0 (2020-02-20) + +- Support full login logic (bloodbare) + # 1.0.3 (2020-01-24) - Auto-tagging and auto-release to NPM diff --git a/projects/grange-core/package.json b/projects/grange-core/package.json index fd18031..9e05e7c 100644 --- a/projects/grange-core/package.json +++ b/projects/grange-core/package.json @@ -1,6 +1,6 @@ { - "name": "@guillotinaweb/grange-core", - "version": "1.0.3", + "name": "grange-core", + "version": "1.1.0", "license": "MIT", "author": { "name": "Eric Brehault", diff --git a/projects/grange-core/src/lib/api.service.spec.ts b/projects/grange-core/src/lib/api.service.spec.ts index 41a020c..1fa3732 100644 --- a/projects/grange-core/src/lib/api.service.spec.ts +++ b/projects/grange-core/src/lib/api.service.spec.ts @@ -29,7 +29,8 @@ describe('APIService', () => { { provide: 'CONFIGURATION', useValue: { BACKEND_URL: 'http://fake/Plone', - }} + }}, + { provide: 'LANG', useValue: 'en'}, ] }); diff --git a/projects/grange-core/src/lib/authentication.service.spec.ts b/projects/grange-core/src/lib/authentication.service.spec.ts index ba2e3b8..82d2e9a 100644 --- a/projects/grange-core/src/lib/authentication.service.spec.ts +++ b/projects/grange-core/src/lib/authentication.service.spec.ts @@ -1,9 +1,11 @@ import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; +import { TestBed, async } from '@angular/core/testing'; import { AuthenticatedStatus, Error, PasswordResetInfo } from './interfaces'; import { AuthenticationService, getError } from './authentication.service'; import { ConfigurationService } from './configuration.service'; +import { ReCaptchaV3Service } from './recaptcha_v3.service'; +import { filter } from 'rxjs/operators'; describe('AuthenticationService', () => { beforeEach(() => { @@ -12,11 +14,13 @@ describe('AuthenticationService', () => { providers: [ AuthenticationService, ConfigurationService, + ReCaptchaV3Service, { provide: 'CONFIGURATION', useValue: { BACKEND_URL: 'http://fake/Plone', } }, + { provide: 'LANG', useValue: 'en'}, ] }); }); @@ -89,15 +93,14 @@ describe('AuthenticationService', () => { }); it('should logout', () => { - const service = TestBed.get(AuthenticationService); + const service: AuthenticationService = TestBed.get(AuthenticationService); // fake login localStorage.setItem('auth', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZnVsbG5hbWUiOiJGb28gYmFyIiwiZ' + 'XhwaXJlcyI6MTQ2NjE0MDA2Ni42MzQ5ODYsInR5cGUiOiJKV1QiLCJhbGdvcml0aG0iOiJIUzI1NiJ9.D9EL5A9xD1z3E_HPecXA-Ee7kKlljYvpDtan69KHwZ8'); localStorage.setItem('auth_time', (new Date()).toISOString()); - service.logout(); - + service._logout(); expect(localStorage.getItem('auth')).toEqual(null); expect(localStorage.getItem('auth_time')).toEqual(null); }); diff --git a/projects/grange-core/src/lib/authentication.service.ts b/projects/grange-core/src/lib/authentication.service.ts index 2cf9d05..88c488b 100644 --- a/projects/grange-core/src/lib/authentication.service.ts +++ b/projects/grange-core/src/lib/authentication.service.ts @@ -4,9 +4,9 @@ import { HttpHeaders, } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, from } from 'rxjs'; +import { BehaviorSubject, Observable, from, throwError } from 'rxjs'; import { AuthenticatedStatus, Error, PasswordResetInfo, UserInfoTokenParts, LoginToken } from './interfaces'; -import { tap, catchError, map, concatMap } from 'rxjs/operators'; +import { catchError, map, concatMap } from 'rxjs/operators'; import { ReCaptchaV3Service } from './recaptcha_v3.service'; import { ConfigurationService } from './configuration.service'; @@ -116,7 +116,7 @@ export class AuthenticationService { protected error(errorResponse: HttpErrorResponse): Observable { const error: Error = getError(errorResponse); - return Observable.throw(error); + return throwError(error); } // LOGIN LOGIC @@ -129,7 +129,7 @@ export class AuthenticationService { return this.doLogin(login, password, path, token); }), catchError((err) => { - return Observable.throw(err); + return throwError(err); })); } else { return this.doLogin(login, password, path); @@ -177,14 +177,14 @@ export class AuthenticationService { error: error.message, }); } - return Observable.throw(error); + return throwError(error); }) ); } // LOGOUT LOGIC - private _logout() { + _logout() { this.cleanBasicCredentials(); localStorage.removeItem('auth'); localStorage.removeItem('auth_time'); @@ -196,7 +196,7 @@ export class AuthenticationService { const url = this.config.get('BACKEND_URL') + `/@logout`; this.http - .post(url, {}, { headers: headers }) + .post(url, {}, { headers }) .pipe( catchError(this.error.bind(this)) ).subscribe( @@ -216,7 +216,7 @@ export class AuthenticationService { const url = this.config.get('BACKEND_URL') + `/@users/${login}/reset-password`; return this.http - .post(url, {}, { headers: headers }) + .post(url, {}, { headers }) .pipe( catchError(this.error.bind(this)) ); @@ -227,10 +227,10 @@ export class AuthenticationService { const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'reset'); return from(promise).pipe( concatMap((token: string) => { - return this.doRequestPasswordReset(login, token) + return this.doRequestPasswordReset(login, token); }), catchError((err) => { - return Observable.throw(err); + return throwError(err); })); } else { return this.doRequestPasswordReset(login); @@ -254,7 +254,7 @@ export class AuthenticationService { this.config.get('BACKEND_URL') + `/@users/${resetInfo.login}/reset-password`; return this.http - .post(url, data, { headers: headers }) + .post(url, data, { headers }) .pipe( catchError(this.error.bind(this)) ); @@ -273,7 +273,7 @@ export class AuthenticationService { this.config.get('BACKEND_URL') + `/@validate_schema/${token}`; return this.http - .post(url, {}, { headers: headers }) + .post(url, {}, { headers }) .pipe( catchError(this.error.bind(this)) ); @@ -284,10 +284,10 @@ export class AuthenticationService { const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'schema'); return from(promise).pipe( concatMap((recaptcha: string) => { - return this.doGetValidationSchema(token, recaptcha) + return this.doGetValidationSchema(token, recaptcha); }), catchError((err) => { - return Observable.throw(err); + return throwError(err); })); } else { return this.doGetValidationSchema(token); @@ -300,7 +300,7 @@ export class AuthenticationService { this.config.get('BACKEND_URL') + `/@validate/${token}`; return this.http - .post(url, model, { headers: headers }) + .post(url, model, { headers }) .pipe( catchError(this.error.bind(this)) ); @@ -312,10 +312,10 @@ export class AuthenticationService { const promise = this.recaptcha.executeAsPromise(this.config.get('RECAPTCHA_TOKEN'), 'validation'); return from(promise).pipe( concatMap((recaptcha: string) => { - return this.doRealValidation(token, model, recaptcha) + return this.doRealValidation(token, model, recaptcha); }), catchError((err) => { - return Observable.throw(err); + return throwError(err); })); } else { return this.doRealValidation(token, model); @@ -327,7 +327,7 @@ export class AuthenticationService { const url = this.config.get('BACKEND_URL') + `/@info`; return this.http - .get(url, { headers: headers }) + .get(url, { headers }) .pipe( catchError(this.error.bind(this)) ); @@ -342,7 +342,7 @@ export class AuthenticationService { return this.doGetInfo(recaptcha); }), catchError((err) => { - return Observable.throw(err); + return throwError(err); })); } else { return this.doGetInfo(); diff --git a/projects/grange-core/src/lib/cache.service.spec.ts b/projects/grange-core/src/lib/cache.service.spec.ts index 9efd952..e752cf9 100644 --- a/projects/grange-core/src/lib/cache.service.spec.ts +++ b/projects/grange-core/src/lib/cache.service.spec.ts @@ -61,6 +61,7 @@ describe('CacheService', () => { CACHE_REFRESH_DELAY: 1000, } }, + { provide: 'LANG', useValue: 'en'}, CacheService, ] }); @@ -152,6 +153,7 @@ describe('CacheService', () => { CACHE_REFRESH_DELAY: 5 } }, + { provide: 'LANG', useValue: 'en'}, CacheService, ] }); @@ -196,6 +198,7 @@ describe('CacheService', () => { CACHE_MAX_SIZE: 2, } }, + { provide: 'LANG', useValue: 'en'}, CacheService, ] }); diff --git a/projects/grange-core/src/lib/resource.service.spec.ts b/projects/grange-core/src/lib/resource.service.spec.ts index 32d184b..1cbce8c 100644 --- a/projects/grange-core/src/lib/resource.service.spec.ts +++ b/projects/grange-core/src/lib/resource.service.spec.ts @@ -3,7 +3,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { TestBed, async } from '@angular/core/testing'; import { NamedFileUpload, NavLink } from './interfaces'; import { Vocabulary } from './vocabularies'; import { APIService } from './api.service'; @@ -29,7 +29,8 @@ describe('ResourceService', () => { useValue: { BACKEND_URL: 'http://fake/Plone' } - } + }, + { provide: 'LANG', useValue: 'en'}, ] }); }); @@ -588,7 +589,7 @@ describe('ResourceService', () => { it( 'should get a file upload', - fakeAsync(() => { + async(() => { const blob: { [key: string]: any } = new Blob([''], { type: 'text/csv' }); blob['name'] = 'filename.csv'; @@ -601,7 +602,6 @@ describe('ResourceService', () => { expect(namedFile['content-type']).toBe('text/csv'); } ); - tick(); }) ); });