mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 10:34:15 +00:00
Added refresh token functionality
This commit is contained in:
@@ -86,7 +86,6 @@
|
|||||||
"@nguniversal/express-engine": "5.0.0-beta.5",
|
"@nguniversal/express-engine": "5.0.0-beta.5",
|
||||||
"@ngx-translate/core": "9.1.1",
|
"@ngx-translate/core": "9.1.1",
|
||||||
"@ngx-translate/http-loader": "2.0.1",
|
"@ngx-translate/http-loader": "2.0.1",
|
||||||
"@types/js-cookie": "^2.1.0",
|
|
||||||
"angular-idle-preload": "2.0.4",
|
"angular-idle-preload": "2.0.4",
|
||||||
"body-parser": "1.18.2",
|
"body-parser": "1.18.2",
|
||||||
"bootstrap": "4.0.0-beta",
|
"bootstrap": "4.0.0-beta",
|
||||||
@@ -102,6 +101,7 @@
|
|||||||
"js-cookie": "2.2.0",
|
"js-cookie": "2.2.0",
|
||||||
"js.clone": "0.0.3",
|
"js.clone": "0.0.3",
|
||||||
"jsonschema": "1.2.2",
|
"jsonschema": "1.2.2",
|
||||||
|
"jwt-decode": "^2.2.0",
|
||||||
"methods": "1.1.2",
|
"methods": "1.1.2",
|
||||||
"morgan": "1.9.0",
|
"morgan": "1.9.0",
|
||||||
"ngx-pagination": "3.0.3",
|
"ngx-pagination": "3.0.3",
|
||||||
@@ -124,6 +124,7 @@
|
|||||||
"@types/express-serve-static-core": "4.11.1",
|
"@types/express-serve-static-core": "4.11.1",
|
||||||
"@types/hammerjs": "2.0.35",
|
"@types/hammerjs": "2.0.35",
|
||||||
"@types/jasmine": "2.8.4",
|
"@types/jasmine": "2.8.4",
|
||||||
|
"@types/js-cookie": "2.1.0",
|
||||||
"@types/memory-cache": "0.2.0",
|
"@types/memory-cache": "0.2.0",
|
||||||
"@types/mime": "2.0.0",
|
"@types/mime": "2.0.0",
|
||||||
"@types/node": "^9.3.0",
|
"@types/node": "^9.3.0",
|
||||||
|
@@ -30,6 +30,8 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s
|
|||||||
|
|
||||||
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
|
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
|
||||||
import { MockMetadataService } from './shared/mocks/mock-metadata-service';
|
import { MockMetadataService } from './shared/mocks/mock-metadata-service';
|
||||||
|
import { PlatformServiceStub } from './shared/testing/platform-service-stub';
|
||||||
|
import { PlatformService } from './shared/services/platform.service';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
@@ -56,6 +58,7 @@ describe('App component', () => {
|
|||||||
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
||||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||||
{ provide: MetadataService, useValue: new MockMetadataService() },
|
{ provide: MetadataService, useValue: new MockMetadataService() },
|
||||||
|
{ provide: PlatformService, useValue: new PlatformServiceStub() },
|
||||||
AppComponent
|
AppComponent
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@@ -8,7 +8,7 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
|
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
import { AuthSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
import { AuthStatusResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -37,8 +37,8 @@ export class AuthRequestService extends HALEndpointService {
|
|||||||
errorResponse.flatMap((response: ErrorResponse) =>
|
errorResponse.flatMap((response: ErrorResponse) =>
|
||||||
Observable.throw(new Error(response.errorMessage))),
|
Observable.throw(new Error(response.errorMessage))),
|
||||||
successResponse
|
successResponse
|
||||||
.filter((response: AuthSuccessResponse) => isNotEmpty(response))
|
.filter((response: AuthStatusResponse) => isNotEmpty(response))
|
||||||
.map((response: AuthSuccessResponse) => response.response)
|
.map((response: AuthStatusResponse) => response.response)
|
||||||
.distinctUntilChanged());
|
.distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,25 +2,15 @@ import { Inject, Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { AuthObjectFactory } from './auth-object-factory';
|
import { AuthObjectFactory } from './auth-object-factory';
|
||||||
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
||||||
import {
|
import { AuthStatusResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
AuthErrorResponse,
|
|
||||||
AuthStatusResponse,
|
|
||||||
AuthSuccessResponse, ConfigSuccessResponse, ErrorResponse,
|
|
||||||
RestResponse
|
|
||||||
} from '../cache/response-cache.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { ConfigObject } from '../shared/config/config.model';
|
|
||||||
import { ConfigType } from '../shared/config/config-type';
|
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { ResponseParsingService } from '../data/parsing.service';
|
import { ResponseParsingService } from '../data/parsing.service';
|
||||||
import { RestRequest } from '../data/request.models';
|
import { RestRequest } from '../data/request.models';
|
||||||
import { AuthType } from './auth-type';
|
import { AuthType } from './auth-type';
|
||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
|
||||||
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -29,18 +19,15 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
|
|||||||
protected objectFactory = AuthObjectFactory;
|
protected objectFactory = AuthObjectFactory;
|
||||||
protected toCache = false;
|
protected toCache = false;
|
||||||
|
|
||||||
constructor(
|
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
protected objectCache: ObjectCacheService,) {
|
||||||
protected objectCache: ObjectCacheService,
|
super();
|
||||||
) { super();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||||
const response = this.process<AuthStatus, AuthType>(data.payload, request.href);
|
const response = this.process<AuthStatus, AuthType>(data.payload, request.href);
|
||||||
return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode);
|
return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode);
|
||||||
} else if (isEmpty(data.payload) && isNotEmpty(data.headers.get('authorization')) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
|
||||||
return new AuthSuccessResponse(new AuthTokenInfo(data.headers.get('authorization')), data.statusCode);
|
|
||||||
} else {
|
} else {
|
||||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,9 @@ export const AuthActionTypes = {
|
|||||||
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
|
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
|
||||||
REDIRECT_TOKEN_EXPIRED: type('dspace/auth/REDIRECT_TOKEN_EXPIRED'),
|
REDIRECT_TOKEN_EXPIRED: type('dspace/auth/REDIRECT_TOKEN_EXPIRED'),
|
||||||
REDIRECT_AUTHENTICATION_REQUIRED: type('dspace/auth/REDIRECT_AUTHENTICATION_REQUIRED'),
|
REDIRECT_AUTHENTICATION_REQUIRED: type('dspace/auth/REDIRECT_AUTHENTICATION_REQUIRED'),
|
||||||
|
REFRESH_TOKEN: type('dspace/auth/REFRESH_TOKEN'),
|
||||||
|
REFRESH_TOKEN_SUCCESS: type('dspace/auth/REFRESH_TOKEN_SUCCESS'),
|
||||||
|
REFRESH_TOKEN_ERROR: type('dspace/auth/REFRESH_TOKEN_ERROR'),
|
||||||
RESET_MESSAGES: type('dspace/auth/RESET_MESSAGES'),
|
RESET_MESSAGES: type('dspace/auth/RESET_MESSAGES'),
|
||||||
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
||||||
LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'),
|
LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'),
|
||||||
@@ -201,6 +204,43 @@ export class RedirectWhenTokenExpiredAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh authentication token.
|
||||||
|
* @class RefreshTokenAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RefreshTokenAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REFRESH_TOKEN;
|
||||||
|
payload: AuthTokenInfo;
|
||||||
|
|
||||||
|
constructor(token: AuthTokenInfo) {
|
||||||
|
this.payload = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh authentication token success.
|
||||||
|
* @class RefreshTokenSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RefreshTokenSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REFRESH_TOKEN_SUCCESS;
|
||||||
|
payload: AuthTokenInfo;
|
||||||
|
|
||||||
|
constructor(token: AuthTokenInfo) {
|
||||||
|
this.payload = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh authentication token error.
|
||||||
|
* @class RefreshTokenErrorAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RefreshTokenErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REFRESH_TOKEN_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign up.
|
* Sign up.
|
||||||
* @class RegistrationAction
|
* @class RegistrationAction
|
||||||
|
@@ -18,7 +18,7 @@ import {
|
|||||||
AuthenticationErrorAction,
|
AuthenticationErrorAction,
|
||||||
AuthenticationSuccessAction, CheckAuthenticationTokenErrorAction,
|
AuthenticationSuccessAction, CheckAuthenticationTokenErrorAction,
|
||||||
LogOutErrorAction,
|
LogOutErrorAction,
|
||||||
LogOutSuccessAction, RegistrationAction,
|
LogOutSuccessAction, RefreshTokenAction, RefreshTokenErrorAction, RefreshTokenSuccessAction, RegistrationAction,
|
||||||
RegistrationErrorAction,
|
RegistrationErrorAction,
|
||||||
RegistrationSuccessAction
|
RegistrationSuccessAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
@@ -41,11 +41,11 @@ export class AuthEffects {
|
|||||||
.ofType(AuthActionTypes.AUTHENTICATE)
|
.ofType(AuthActionTypes.AUTHENTICATE)
|
||||||
.switchMap((action: AuthenticateAction) => {
|
.switchMap((action: AuthenticateAction) => {
|
||||||
return this.authService.authenticate(action.payload.email, action.payload.password)
|
return this.authService.authenticate(action.payload.email, action.payload.password)
|
||||||
|
.first()
|
||||||
.map((response: AuthStatus) => new AuthenticationSuccessAction(response.token))
|
.map((response: AuthStatus) => new AuthenticationSuccessAction(response.token))
|
||||||
.catch((error) => Observable.of(new AuthenticationErrorAction(error)));
|
.catch((error) => Observable.of(new AuthenticationErrorAction(error)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// It means "reacts to this action but don't send another"
|
|
||||||
@Effect()
|
@Effect()
|
||||||
public authenticateSuccess: Observable<Action> = this.actions$
|
public authenticateSuccess: Observable<Action> = this.actions$
|
||||||
.ofType(AuthActionTypes.AUTHENTICATE_SUCCESS)
|
.ofType(AuthActionTypes.AUTHENTICATE_SUCCESS)
|
||||||
@@ -61,6 +61,7 @@ export class AuthEffects {
|
|||||||
.catch((error) => Observable.of(new AuthenticatedErrorAction(error)));
|
.catch((error) => Observable.of(new AuthenticatedErrorAction(error)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// It means "reacts to this action but don't send another"
|
||||||
@Effect({dispatch: false})
|
@Effect({dispatch: false})
|
||||||
public authenticatedError: Observable<Action> = this.actions$
|
public authenticatedError: Observable<Action> = this.actions$
|
||||||
.ofType(AuthActionTypes.AUTHENTICATED_ERROR)
|
.ofType(AuthActionTypes.AUTHENTICATED_ERROR)
|
||||||
@@ -85,6 +86,21 @@ export class AuthEffects {
|
|||||||
.catch((error) => Observable.of(new RegistrationErrorAction(error)));
|
.catch((error) => Observable.of(new RegistrationErrorAction(error)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public refreshToken: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.REFRESH_TOKEN)
|
||||||
|
.switchMap((action: RefreshTokenAction) => {
|
||||||
|
return this.authService.refreshAuthenticationToken(action.payload)
|
||||||
|
.map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token))
|
||||||
|
.catch((error) => Observable.of(new RefreshTokenErrorAction()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// It means "reacts to this action but don't send another"
|
||||||
|
@Effect({dispatch: false})
|
||||||
|
public refreshTokenSuccess: Observable<Action> = this.actions$
|
||||||
|
.ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS)
|
||||||
|
.do((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the store is rehydrated in the browser,
|
* When the store is rehydrated in the browser,
|
||||||
* clear a possible invalid token or authentication errors
|
* clear a possible invalid token or authentication errors
|
||||||
|
@@ -1,40 +1,55 @@
|
|||||||
import { Injectable, Injector } from '@angular/core';
|
import { Injectable, Injector } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse,
|
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse,
|
||||||
HttpErrorResponse
|
HttpErrorResponse, HttpResponseBase
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Rx';
|
import { Observable } from 'rxjs/Rx';
|
||||||
import 'rxjs/add/observable/throw'
|
import 'rxjs/add/observable/throw'
|
||||||
import 'rxjs/add/operator/catch';
|
import 'rxjs/add/operator/catch';
|
||||||
|
|
||||||
|
import { find } from 'lodash';
|
||||||
|
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { AuthError } from './models/auth-error.model';
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty, isUndefined } from '../../shared/empty.util';
|
||||||
import { RedirectWhenTokenExpiredAction } from './auth.actions';
|
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
// Intercetor is called twice per request,
|
||||||
|
// so to prevent RefreshTokenAction is dispatched twice
|
||||||
|
// we're creating a refresh token request list
|
||||||
|
protected refreshTokenRequestUrls = [];
|
||||||
|
|
||||||
constructor(private inj: Injector, private store: Store<AppState>) { }
|
constructor(private inj: Injector, private store: Store<AppState>) { }
|
||||||
|
|
||||||
private isUnauthorized(status: number): boolean {
|
private isUnauthorized(response: HttpResponseBase): boolean {
|
||||||
return status === 401 || status === 403;
|
// invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||||
|
return response.status === 401;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAuthRequest(url: string): boolean {
|
private isSuccess(response: HttpResponseBase): boolean {
|
||||||
return url.endsWith('/authn/login') || url.endsWith('/authn/logout') || url.endsWith('/authn/status');
|
return response.status === 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLoginResponse(url: string): boolean {
|
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
return url.endsWith('/authn/login');
|
return http.url
|
||||||
|
&& (http.url.endsWith('/authn/login')
|
||||||
|
|| http.url.endsWith('/authn/logout')
|
||||||
|
|| http.url.endsWith('/authn/status'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLogoutResponse(url: string): boolean {
|
private isLoginResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
return url.endsWith('/authn/logout');
|
return http.url && http.url.endsWith('/authn/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLogoutResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
|
return http.url && http.url.endsWith('/authn/logout');
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus {
|
private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus {
|
||||||
@@ -55,27 +70,45 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
|
|
||||||
const authService = this.inj.get(AuthService);
|
const authService = this.inj.get(AuthService);
|
||||||
|
|
||||||
// Get the auth header from the service.
|
const token = authService.getToken();
|
||||||
const Authorization = authService.getAuthHeader();
|
|
||||||
|
|
||||||
let authReq;
|
let newReq;
|
||||||
if (!this.isAuthRequest(req.url) && isNotEmpty(Authorization)) {
|
// Intercept a request that is not to the authentication endpoint
|
||||||
|
if (!this.isAuthRequest(req) && isNotEmpty(token)) {
|
||||||
|
authService.isTokenExpiring()
|
||||||
|
.filter((isExpiring) => isExpiring)
|
||||||
|
.subscribe(() => {
|
||||||
|
// If the current request url is already in the refresh token request list, skip it
|
||||||
|
if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) {
|
||||||
|
// When a token is about to expire, refresh it
|
||||||
|
this.store.dispatch(new RefreshTokenAction(token));
|
||||||
|
this.refreshTokenRequestUrls.push(req.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Get the auth header from the service.
|
||||||
|
const Authorization = authService.buildAuthHeader(token);
|
||||||
// Clone the request to add the new header.
|
// Clone the request to add the new header.
|
||||||
authReq = req.clone({headers: req.headers.set('authorization', Authorization)});
|
newReq = req.clone({headers: req.headers.set('authorization', Authorization)});
|
||||||
} else {
|
} else {
|
||||||
authReq = req;
|
newReq = req;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass on the cloned request instead of the original request.
|
// Pass on the new request instead of the original request.
|
||||||
return next.handle(authReq)
|
return next.handle(newReq)
|
||||||
.map((response) => {
|
.map((response) => {
|
||||||
if (response instanceof HttpResponse && response.status === 200 && (this.isLoginResponse(response.url) || this.isLogoutResponse(response.url))) {
|
// Intercept a Login/Logout response
|
||||||
|
if (response instanceof HttpResponse && this.isSuccess(response) && (this.isLoginResponse(response) || this.isLogoutResponse(response))) {
|
||||||
|
// It's a success Login/Logout response
|
||||||
let authRes: HttpResponse<any>;
|
let authRes: HttpResponse<any>;
|
||||||
if (this.isLoginResponse(response.url)) {
|
if (this.isLoginResponse(response)) {
|
||||||
const token = response.headers.get('authorization');
|
// login successfully
|
||||||
const expires = response.headers.get('expires');
|
const newToken = response.headers.get('authorization');
|
||||||
authRes = response.clone({body: this.makeAuthStatusObject(true, token)});
|
authRes = response.clone({body: this.makeAuthStatusObject(true, newToken)});
|
||||||
|
|
||||||
|
// clean eventually refresh Requests list
|
||||||
|
this.refreshTokenRequestUrls = [];
|
||||||
} else {
|
} else {
|
||||||
|
// logout successfully
|
||||||
authRes = response.clone({body: this.makeAuthStatusObject(false)});
|
authRes = response.clone({body: this.makeAuthStatusObject(false)});
|
||||||
}
|
}
|
||||||
return authRes;
|
return authRes;
|
||||||
@@ -84,10 +117,12 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error, caught) => {
|
.catch((error, caught) => {
|
||||||
// Intercept an unauthorized error response
|
// Intercept an error response
|
||||||
if (error instanceof HttpErrorResponse && this.isUnauthorized(error.status)) {
|
if (error instanceof HttpErrorResponse) {
|
||||||
// Checks if is a response from a request to an authentication endpoint
|
// Checks if is a response from a request to an authentication endpoint
|
||||||
if (this.isAuthRequest(error.url)) {
|
if (this.isAuthRequest(error)) {
|
||||||
|
// clean eventually refresh Requests list
|
||||||
|
this.refreshTokenRequestUrls = [];
|
||||||
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
|
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
|
||||||
const authResponse = new HttpResponse({
|
const authResponse = new HttpResponse({
|
||||||
body: this.makeAuthStatusObject(false, null, error.error),
|
body: this.makeAuthStatusObject(false, null, error.error),
|
||||||
@@ -97,14 +132,15 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
url: error.url
|
url: error.url
|
||||||
});
|
});
|
||||||
return Observable.of(authResponse);
|
return Observable.of(authResponse);
|
||||||
} else {
|
} else if (this.isUnauthorized(error)) {
|
||||||
|
// The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||||
// Redirect to the login route
|
// Redirect to the login route
|
||||||
this.store.dispatch(new RedirectWhenTokenExpiredAction('Your session has expired. Please log in again.'));
|
this.store.dispatch(new RedirectWhenTokenExpiredAction('Your session has expired. Please log in again.'));
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Return error response as is.
|
// Return error response as is.
|
||||||
return Observable.throw(error);
|
return Observable.throw(error);
|
||||||
}
|
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -32,6 +32,9 @@ export interface AuthState {
|
|||||||
// redirect url after login
|
// redirect url after login
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
|
|
||||||
|
// true when refreshing token
|
||||||
|
refreshing?: boolean;
|
||||||
|
|
||||||
// the authenticated user
|
// the authenticated user
|
||||||
user?: Eperson;
|
user?: Eperson;
|
||||||
}
|
}
|
||||||
@@ -108,12 +111,14 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.LOG_OUT_SUCCESS:
|
case AuthActionTypes.LOG_OUT_SUCCESS:
|
||||||
|
case AuthActionTypes.REFRESH_TOKEN_ERROR:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
|
refreshing: false,
|
||||||
user: undefined
|
user: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,6 +143,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
case AuthActionTypes.REGISTRATION_SUCCESS:
|
case AuthActionTypes.REGISTRATION_SUCCESS:
|
||||||
return state;
|
return state;
|
||||||
|
|
||||||
|
case AuthActionTypes.REFRESH_TOKEN:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
refreshing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.REFRESH_TOKEN_SUCCESS:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
refreshing: false,
|
||||||
|
});
|
||||||
|
|
||||||
case AuthActionTypes.RESET_MESSAGES:
|
case AuthActionTypes.RESET_MESSAGES:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: state.authenticated,
|
authenticated: state.authenticated,
|
||||||
@@ -159,7 +174,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the user is authenticated.
|
* Returns true if the user is authenticated.
|
||||||
* @function isAuthenticated
|
* @function _isAuthenticated
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
@@ -167,7 +182,7 @@ export const _isAuthenticated = (state: AuthState) => state.authenticated;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the authenticated has loaded.
|
* Returns true if the authenticated has loaded.
|
||||||
* @function isAuthenticatedLoaded
|
* @function _isAuthenticatedLoaded
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
@@ -175,7 +190,7 @@ export const _isAuthenticatedLoaded = (state: AuthState) => state.loaded;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the users state
|
* Return the users state
|
||||||
* @function getAuthenticatedUser
|
* @function _getAuthenticatedUser
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {User}
|
* @returns {User}
|
||||||
*/
|
*/
|
||||||
@@ -183,7 +198,7 @@ export const _getAuthenticatedUser = (state: AuthState) => state.user;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication error.
|
* Returns the authentication error.
|
||||||
* @function getAuthenticationError
|
* @function _getAuthenticationError
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
@@ -191,7 +206,7 @@ export const _getAuthenticationError = (state: AuthState) => state.error;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication info message.
|
* Returns the authentication info message.
|
||||||
* @function getAuthenticationInfo
|
* @function _getAuthenticationInfo
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
@@ -199,15 +214,23 @@ export const _getAuthenticationInfo = (state: AuthState) => state.info;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if request is in progress.
|
* Returns true if request is in progress.
|
||||||
* @function isLoading
|
* @function _isLoading
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export const _isLoading = (state: AuthState) => state.loading;
|
export const _isLoading = (state: AuthState) => state.loading;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a refresh token request is in progress.
|
||||||
|
* @function _isRefreshing
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const _isRefreshing = (state: AuthState) => state.refreshing;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the sign out error.
|
* Returns the sign out error.
|
||||||
* @function getLogOutError
|
* @function _getLogOutError
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
@@ -215,7 +238,7 @@ export const _getLogOutError = (state: AuthState) => state.error;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the sign up error.
|
* Returns the sign up error.
|
||||||
* @function getRegistrationError
|
* @function _getRegistrationError
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
@@ -223,7 +246,7 @@ export const _getRegistrationError = (state: AuthState) => state.error;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the redirect url.
|
* Returns the redirect url.
|
||||||
* @function getRedirectUrl
|
* @function _getRedirectUrl
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
@@ -11,12 +11,13 @@ import { AuthStatus } from './models/auth-status.model';
|
|||||||
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
||||||
import { isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
import { isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
||||||
import { CookieService } from '../../shared/services/cookie.service';
|
import { CookieService } from '../../shared/services/cookie.service';
|
||||||
import { getRedirectUrl, isAuthenticated } from './selectors';
|
import { getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
||||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
||||||
import { RouterReducerState } from '@ngrx/router-store';
|
import { RouterReducerState } from '@ngrx/router-store';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { CookieAttributes } from 'js-cookie';
|
||||||
|
|
||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
/**
|
/**
|
||||||
@@ -110,11 +111,30 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if token is present into storage
|
* Checks if token is present into storage and is not expired
|
||||||
*/
|
*/
|
||||||
public checkAuthenticationToken(): Observable<AuthTokenInfo> {
|
public checkAuthenticationToken(): Observable<AuthTokenInfo> {
|
||||||
const token = this.getToken();
|
const token = this.getToken();
|
||||||
return isNotEmpty(token) ? Observable.of(token) : Observable.throw(false);
|
return isNotEmpty(token) && !this.isTokenExpired() ? Observable.of(token) : Observable.throw(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if token is present into storage
|
||||||
|
*/
|
||||||
|
public refreshAuthenticationToken(token: AuthTokenInfo): Observable<AuthTokenInfo> {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Accept', 'application/json');
|
||||||
|
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||||
|
options.headers = headers;
|
||||||
|
return this.authRequestService.postToEndpoint('login', {}, options)
|
||||||
|
.map((status: AuthStatus) => {
|
||||||
|
if (status.authenticated) {
|
||||||
|
return status.token;
|
||||||
|
} else {
|
||||||
|
throw(new Error('Not authenticated'));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,8 +180,7 @@ export class AuthService {
|
|||||||
* Retrieve authentication token info and make authorization header
|
* Retrieve authentication token info and make authorization header
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
public getAuthHeader(): string {
|
public buildAuthHeader(token): string {
|
||||||
const token = this.getToken();
|
|
||||||
return (this._authenticated && isNotNull(token)) ? `Bearer ${token.accessToken}` : '';
|
return (this._authenticated && isNotNull(token)) ? `Bearer ${token.accessToken}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +198,31 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a token is about to expire
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isTokenExpiring(): Observable<boolean> {
|
||||||
|
return this.store.select(isTokenRefreshing)
|
||||||
|
.first()
|
||||||
|
.map((isRefreshing: boolean) => {
|
||||||
|
if (this.isTokenExpired() || isRefreshing) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
const token = this.getToken();
|
||||||
|
return token.expires - (60 * 5 * 1000) < Date.now();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a token is expired
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isTokenExpired(): boolean {
|
||||||
|
const token = this.getToken();
|
||||||
|
return token && token.expires < Date.now();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Save authentication token info
|
* Save authentication token info
|
||||||
*
|
*
|
||||||
@@ -186,7 +230,9 @@ export class AuthService {
|
|||||||
* @returns {AuthTokenInfo}
|
* @returns {AuthTokenInfo}
|
||||||
*/
|
*/
|
||||||
public storeToken(token: AuthTokenInfo) {
|
public storeToken(token: AuthTokenInfo) {
|
||||||
return this.storage.set(TOKENITEM, token);
|
const expires = new Date(token.expires);
|
||||||
|
const options: CookieAttributes = { expires: expires };
|
||||||
|
return this.storage.set(TOKENITEM, token, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,6 +242,14 @@ export class AuthService {
|
|||||||
return this.storage.remove(TOKENITEM);
|
return this.storage.remove(TOKENITEM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace authentication token info with a new one
|
||||||
|
*/
|
||||||
|
public replaceToken(token: AuthTokenInfo) {
|
||||||
|
this.removeToken();
|
||||||
|
return this.storeToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to the login route
|
* Redirect to the login route
|
||||||
*/
|
*/
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
|
import { default as decode } from 'jwt-decode';
|
||||||
|
|
||||||
export const TOKENITEM = 'dsAuthInfo';
|
export const TOKENITEM = 'dsAuthInfo';
|
||||||
|
|
||||||
export class AuthTokenInfo {
|
export class AuthTokenInfo {
|
||||||
public accessToken: string;
|
public accessToken: string;
|
||||||
public expires?: number;
|
public expires: number;
|
||||||
|
|
||||||
constructor(token: string, expiresIn?: number) {
|
constructor(token: string) {
|
||||||
this.accessToken = token.replace('Bearer ', '');
|
this.accessToken = token.replace('Bearer ', '');
|
||||||
if (expiresIn) {
|
const tokenClaims = decode(this.accessToken);
|
||||||
this.expires = expiresIn * 1000 + Date.now();
|
// exp claim is in seconds, convert it se to milliseconds
|
||||||
}
|
this.expires = tokenClaims.exp * 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -74,6 +74,15 @@ export const isAuthenticatedLoaded = createSelector(getAuthState, auth._isAuthen
|
|||||||
*/
|
*/
|
||||||
export const isAuthenticationLoading = createSelector(getAuthState, auth._isLoading);
|
export const isAuthenticationLoading = createSelector(getAuthState, auth._isLoading);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the refresh token request is loading.
|
||||||
|
* @function isTokenRefreshing
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isTokenRefreshing = createSelector(getAuthState, auth._isRefreshing);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the log out error.
|
* Returns the log out error.
|
||||||
* @function getLogOutError
|
* @function getLogOutError
|
||||||
|
19
src/app/core/cache/response-cache.models.ts
vendored
19
src/app/core/cache/response-cache.models.ts
vendored
@@ -77,23 +77,4 @@ export class AuthStatusResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthSuccessResponse extends RestResponse {
|
|
||||||
public toCache = false;
|
|
||||||
constructor(
|
|
||||||
public response: AuthTokenInfo,
|
|
||||||
public statusCode: string
|
|
||||||
) {
|
|
||||||
super(true, statusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AuthErrorResponse extends RestResponse {
|
|
||||||
public toCache = false;
|
|
||||||
constructor(
|
|
||||||
public response: any,
|
|
||||||
public statusCode: string,
|
|
||||||
) {
|
|
||||||
super(true, statusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
@@ -236,7 +236,7 @@ describe('RequestService', () => {
|
|||||||
|
|
||||||
it('should dispatch the request', () => {
|
it('should dispatch the request', () => {
|
||||||
service.configure(request);
|
service.configure(request);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('and it is already cached or pending', () => {
|
describe('and it is already cached or pending', () => {
|
||||||
@@ -254,22 +254,22 @@ describe('RequestService', () => {
|
|||||||
describe('when the request isn\'t a GET request', () => {
|
describe('when the request isn\'t a GET request', () => {
|
||||||
it('should dispatch the request', () => {
|
it('should dispatch the request', () => {
|
||||||
service.configure(testPostRequest);
|
service.configure(testPostRequest);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest, false);
|
||||||
|
|
||||||
service.configure(testPutRequest);
|
service.configure(testPutRequest);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest, false);
|
||||||
|
|
||||||
service.configure(testDeleteRequest);
|
service.configure(testDeleteRequest);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest, false);
|
||||||
|
|
||||||
service.configure(testOptionsRequest);
|
service.configure(testOptionsRequest);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest, false);
|
||||||
|
|
||||||
service.configure(testHeadRequest);
|
service.configure(testHeadRequest);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest, false);
|
||||||
|
|
||||||
service.configure(testPatchRequest);
|
service.configure(testPatchRequest);
|
||||||
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest);
|
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -102,7 +102,7 @@ export class RequestService {
|
|||||||
return isCached || isPending;
|
return isCached || isPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
private dispatchRequest(request: RestRequest, overrideRequest: boolean) {
|
private dispatchRequest(request: RestRequest, overrideRequest: boolean = false) {
|
||||||
this.store.dispatch(new RequestConfigureAction(request));
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
||||||
if (request.method === RestRequestMethod.Get && !overrideRequest) {
|
if (request.method === RestRequestMethod.Get && !overrideRequest) {
|
||||||
|
@@ -7,6 +7,6 @@ export interface DSpaceRESTV2Response {
|
|||||||
_links?: any;
|
_links?: any;
|
||||||
page?: any;
|
page?: any;
|
||||||
},
|
},
|
||||||
headers: HttpHeaders,
|
headers?: HttpHeaders,
|
||||||
statusCode: string
|
statusCode: string
|
||||||
}
|
}
|
||||||
|
@@ -71,6 +71,7 @@ export class DSpaceRESTv2Service {
|
|||||||
console.log(res);
|
console.log(res);
|
||||||
return ({ payload: res.body, headers: res.headers, statusCode: res.statusText })
|
return ({ payload: res.body, headers: res.headers, statusCode: res.statusText })
|
||||||
})
|
})
|
||||||
|
.share()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('Error: ', err);
|
console.log('Error: ', err);
|
||||||
return Observable.throw(err);
|
return Observable.throw(err);
|
||||||
|
@@ -9,6 +9,16 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
import { HeaderState } from './header.reducer';
|
import { HeaderState } from './header.reducer';
|
||||||
import { HeaderToggleAction } from './header.actions';
|
import { HeaderToggleAction } from './header.actions';
|
||||||
|
import { AuthNavMenuComponent } from '../shared/auth-nav-menu/auth-nav-menu.component';
|
||||||
|
import { LogInComponent } from '../shared/log-in/log-in.component';
|
||||||
|
import { LogOutComponent } from '../shared/log-out/log-out.component';
|
||||||
|
import { LoadingComponent } from '../shared/loading/loading.component';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../shared/testing/host-window-service-stub';
|
||||||
|
import { RouterStub } from '../shared/testing/router-stub';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
let comp: HeaderComponent;
|
let comp: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
@@ -19,8 +29,17 @@ describe('HeaderComponent', () => {
|
|||||||
// async beforeEach
|
// async beforeEach
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [StoreModule.forRoot({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot()],
|
imports: [
|
||||||
declarations: [HeaderComponent]
|
StoreModule.forRoot({}),
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
NgbCollapseModule.forRoot(),
|
||||||
|
NoopAnimationsModule,
|
||||||
|
ReactiveFormsModule],
|
||||||
|
declarations: [HeaderComponent, AuthNavMenuComponent, LoadingComponent, LogInComponent, LogOutComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
|
{ provide: Router, useClass: RouterStub },
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.compileComponents(); // compile template and css
|
.compileComponents(); // compile template and css
|
||||||
}));
|
}));
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<ds-loading *ngIf="(loading | async)"></ds-loading>
|
<ds-loading *ngIf="(loading | async)" class="m-5"></ds-loading>
|
||||||
<form *ngIf="!(loading | async)" class="form-login px-4 py-3" (ngSubmit)="submit()" [formGroup]="form" novalidate>
|
<form *ngIf="!(loading | async)" class="form-login px-4 py-3" (ngSubmit)="submit()" [formGroup]="form" novalidate>
|
||||||
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
||||||
<input id="inputEmail"
|
<input id="inputEmail"
|
||||||
|
@@ -1,18 +1,13 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
|
||||||
// @ngrx
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
// rxjs
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import 'rxjs/add/operator/filter';
|
import 'rxjs/add/operator/filter';
|
||||||
import 'rxjs/add/operator/takeWhile';
|
import 'rxjs/add/operator/takeWhile';
|
||||||
|
|
||||||
// actions
|
|
||||||
import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../core/auth/auth.actions';
|
import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../core/auth/auth.actions';
|
||||||
|
|
||||||
// reducers
|
|
||||||
import {
|
import {
|
||||||
getAuthenticationError, getAuthenticationInfo,
|
getAuthenticationError, getAuthenticationInfo,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
@@ -16,4 +16,8 @@ export class HostWindowServiceStub {
|
|||||||
isXs(): Observable<boolean> {
|
isXs(): Observable<boolean> {
|
||||||
return Observable.of(this.width < 576);
|
return Observable.of(this.width < 576);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isMobileView(): Observable<boolean> {
|
||||||
|
return this.isXs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
src/app/shared/testing/platform-service-stub.ts
Normal file
12
src/app/shared/testing/platform-service-stub.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
// declare a stub service
|
||||||
|
export class PlatformServiceStub {
|
||||||
|
|
||||||
|
public get isBrowser(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isServer(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -193,7 +193,7 @@
|
|||||||
version "2.8.4"
|
version "2.8.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.4.tgz#5528fb5e53f1b27594f81f18debb7eab8dc532cb"
|
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.4.tgz#5528fb5e53f1b27594f81f18debb7eab8dc532cb"
|
||||||
|
|
||||||
"@types/js-cookie@^2.1.0":
|
"@types/js-cookie@2.1.0":
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.1.0.tgz#a8916246aa994db646c66d54c854916213300a51"
|
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.1.0.tgz#a8916246aa994db646c66d54c854916213300a51"
|
||||||
|
|
||||||
@@ -4505,6 +4505,10 @@ jszip@^3.1.3:
|
|||||||
pako "~1.0.2"
|
pako "~1.0.2"
|
||||||
readable-stream "~2.0.6"
|
readable-stream "~2.0.6"
|
||||||
|
|
||||||
|
jwt-decode@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
|
||||||
|
|
||||||
karma-chrome-launcher@2.2.0:
|
karma-chrome-launcher@2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf"
|
resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf"
|
||||||
|
Reference in New Issue
Block a user