Added refresh token functionality

This commit is contained in:
Giuseppe Digilio
2018-02-26 15:41:07 +01:00
parent 2637f1c28e
commit 696b8b73f5
22 changed files with 301 additions and 114 deletions

View File

@@ -86,7 +86,6 @@
"@nguniversal/express-engine": "5.0.0-beta.5",
"@ngx-translate/core": "9.1.1",
"@ngx-translate/http-loader": "2.0.1",
"@types/js-cookie": "^2.1.0",
"angular-idle-preload": "2.0.4",
"body-parser": "1.18.2",
"bootstrap": "4.0.0-beta",
@@ -102,6 +101,7 @@
"js-cookie": "2.2.0",
"js.clone": "0.0.3",
"jsonschema": "1.2.2",
"jwt-decode": "^2.2.0",
"methods": "1.1.2",
"morgan": "1.9.0",
"ngx-pagination": "3.0.3",
@@ -124,6 +124,7 @@
"@types/express-serve-static-core": "4.11.1",
"@types/hammerjs": "2.0.35",
"@types/jasmine": "2.8.4",
"@types/js-cookie": "2.1.0",
"@types/memory-cache": "0.2.0",
"@types/mime": "2.0.0",
"@types/node": "^9.3.0",

View File

@@ -30,6 +30,8 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
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 fixture: ComponentFixture<AppComponent>;
@@ -56,6 +58,7 @@ describe('App component', () => {
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
{ provide: MetadataService, useValue: new MockMetadataService() },
{ provide: PlatformService, useValue: new PlatformServiceStub() },
AppComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@@ -8,7 +8,7 @@ import { Observable } from 'rxjs/Observable';
import { isNotEmpty } from '../../shared/empty.util';
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
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';
@Injectable()
@@ -37,8 +37,8 @@ export class AuthRequestService extends HALEndpointService {
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(response.errorMessage))),
successResponse
.filter((response: AuthSuccessResponse) => isNotEmpty(response))
.map((response: AuthSuccessResponse) => response.response)
.filter((response: AuthStatusResponse) => isNotEmpty(response))
.map((response: AuthStatusResponse) => response.response)
.distinctUntilChanged());
}

View File

@@ -2,25 +2,15 @@ import { Inject, Injectable } from '@angular/core';
import { AuthObjectFactory } from './auth-object-factory';
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import {
AuthErrorResponse,
AuthStatusResponse,
AuthSuccessResponse, ConfigSuccessResponse, ErrorResponse,
RestResponse
} from '../cache/response-cache.models';
import { AuthStatusResponse, RestResponse } from '../cache/response-cache.models';
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 { 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 { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models';
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';
@Injectable()
@@ -29,18 +19,15 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
protected objectFactory = AuthObjectFactory;
protected toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) { super();
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,) {
super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
const response = this.process<AuthStatus, AuthType>(data.payload, request.href);
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 {
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
}

View File

@@ -19,6 +19,9 @@ export const AuthActionTypes = {
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
REDIRECT_TOKEN_EXPIRED: type('dspace/auth/REDIRECT_TOKEN_EXPIRED'),
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'),
LOG_OUT: type('dspace/auth/LOG_OUT'),
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.
* @class RegistrationAction

View File

@@ -18,7 +18,7 @@ import {
AuthenticationErrorAction,
AuthenticationSuccessAction, CheckAuthenticationTokenErrorAction,
LogOutErrorAction,
LogOutSuccessAction, RegistrationAction,
LogOutSuccessAction, RefreshTokenAction, RefreshTokenErrorAction, RefreshTokenSuccessAction, RegistrationAction,
RegistrationErrorAction,
RegistrationSuccessAction
} from './auth.actions';
@@ -41,11 +41,11 @@ export class AuthEffects {
.ofType(AuthActionTypes.AUTHENTICATE)
.switchMap((action: AuthenticateAction) => {
return this.authService.authenticate(action.payload.email, action.payload.password)
.first()
.map((response: AuthStatus) => new AuthenticationSuccessAction(response.token))
.catch((error) => Observable.of(new AuthenticationErrorAction(error)));
});
// It means "reacts to this action but don't send another"
@Effect()
public authenticateSuccess: Observable<Action> = this.actions$
.ofType(AuthActionTypes.AUTHENTICATE_SUCCESS)
@@ -61,6 +61,7 @@ export class AuthEffects {
.catch((error) => Observable.of(new AuthenticatedErrorAction(error)));
});
// It means "reacts to this action but don't send another"
@Effect({dispatch: false})
public authenticatedError: Observable<Action> = this.actions$
.ofType(AuthActionTypes.AUTHENTICATED_ERROR)
@@ -85,6 +86,21 @@ export class AuthEffects {
.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,
* clear a possible invalid token or authentication errors

View File

@@ -1,40 +1,55 @@
import { Injectable, Injector } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse,
HttpErrorResponse
HttpErrorResponse, HttpResponseBase
} from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/observable/throw'
import 'rxjs/add/operator/catch';
import { find } from 'lodash';
import { AppState } from '../../app.reducer';
import { AuthError } from './models/auth-error.model';
import { AuthService } from './auth.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { isNotEmpty } from '../../shared/empty.util';
import { RedirectWhenTokenExpiredAction } from './auth.actions';
import { isNotEmpty, isUndefined } from '../../shared/empty.util';
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
import { Store } from '@ngrx/store';
@Injectable()
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>) { }
private isUnauthorized(status: number): boolean {
return status === 401 || status === 403;
private isUnauthorized(response: HttpResponseBase): boolean {
// invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons
return response.status === 401;
}
private isAuthRequest(url: string): boolean {
return url.endsWith('/authn/login') || url.endsWith('/authn/logout') || url.endsWith('/authn/status');
private isSuccess(response: HttpResponseBase): boolean {
return response.status === 200;
}
private isLoginResponse(url: string): boolean {
return url.endsWith('/authn/login');
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
return http.url
&& (http.url.endsWith('/authn/login')
|| http.url.endsWith('/authn/logout')
|| http.url.endsWith('/authn/status'));
}
private isLogoutResponse(url: string): boolean {
return url.endsWith('/authn/logout');
private isLoginResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
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 {
@@ -55,27 +70,45 @@ export class AuthInterceptor implements HttpInterceptor {
const authService = this.inj.get(AuthService);
// Get the auth header from the service.
const Authorization = authService.getAuthHeader();
const token = authService.getToken();
let authReq;
if (!this.isAuthRequest(req.url) && isNotEmpty(Authorization)) {
let newReq;
// 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.
authReq = req.clone({headers: req.headers.set('authorization', Authorization)});
newReq = req.clone({headers: req.headers.set('authorization', Authorization)});
} else {
authReq = req;
newReq = req;
}
// Pass on the cloned request instead of the original request.
return next.handle(authReq)
// Pass on the new request instead of the original request.
return next.handle(newReq)
.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>;
if (this.isLoginResponse(response.url)) {
const token = response.headers.get('authorization');
const expires = response.headers.get('expires');
authRes = response.clone({body: this.makeAuthStatusObject(true, token)});
if (this.isLoginResponse(response)) {
// login successfully
const newToken = response.headers.get('authorization');
authRes = response.clone({body: this.makeAuthStatusObject(true, newToken)});
// clean eventually refresh Requests list
this.refreshTokenRequestUrls = [];
} else {
// logout successfully
authRes = response.clone({body: this.makeAuthStatusObject(false)});
}
return authRes;
@@ -84,10 +117,12 @@ export class AuthInterceptor implements HttpInterceptor {
}
})
.catch((error, caught) => {
// Intercept an unauthorized error response
if (error instanceof HttpErrorResponse && this.isUnauthorized(error.status)) {
// Intercept an error response
if (error instanceof HttpErrorResponse) {
// 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.
const authResponse = new HttpResponse({
body: this.makeAuthStatusObject(false, null, error.error),
@@ -97,14 +132,15 @@ export class AuthInterceptor implements HttpInterceptor {
url: error.url
});
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
this.store.dispatch(new RedirectWhenTokenExpiredAction('Your session has expired. Please log in again.'));
}
} else {
}
// Return error response as is.
return Observable.throw(error);
}
}) as any;
}
}

View File

@@ -32,6 +32,9 @@ export interface AuthState {
// redirect url after login
redirectUrl?: string;
// true when refreshing token
refreshing?: boolean;
// the authenticated user
user?: Eperson;
}
@@ -108,12 +111,14 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
});
case AuthActionTypes.LOG_OUT_SUCCESS:
case AuthActionTypes.REFRESH_TOKEN_ERROR:
return Object.assign({}, state, {
authenticated: false,
error: undefined,
loaded: false,
loading: false,
info: undefined,
refreshing: false,
user: undefined
});
@@ -138,6 +143,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
case AuthActionTypes.REGISTRATION_SUCCESS:
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:
return Object.assign({}, state, {
authenticated: state.authenticated,
@@ -159,7 +174,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
/**
* Returns true if the user is authenticated.
* @function isAuthenticated
* @function _isAuthenticated
* @param {State} state
* @returns {boolean}
*/
@@ -167,7 +182,7 @@ export const _isAuthenticated = (state: AuthState) => state.authenticated;
/**
* Returns true if the authenticated has loaded.
* @function isAuthenticatedLoaded
* @function _isAuthenticatedLoaded
* @param {State} state
* @returns {boolean}
*/
@@ -175,7 +190,7 @@ export const _isAuthenticatedLoaded = (state: AuthState) => state.loaded;
/**
* Return the users state
* @function getAuthenticatedUser
* @function _getAuthenticatedUser
* @param {State} state
* @returns {User}
*/
@@ -183,7 +198,7 @@ export const _getAuthenticatedUser = (state: AuthState) => state.user;
/**
* Returns the authentication error.
* @function getAuthenticationError
* @function _getAuthenticationError
* @param {State} state
* @returns {string}
*/
@@ -191,7 +206,7 @@ export const _getAuthenticationError = (state: AuthState) => state.error;
/**
* Returns the authentication info message.
* @function getAuthenticationInfo
* @function _getAuthenticationInfo
* @param {State} state
* @returns {string}
*/
@@ -199,15 +214,23 @@ export const _getAuthenticationInfo = (state: AuthState) => state.info;
/**
* Returns true if request is in progress.
* @function isLoading
* @function _isLoading
* @param {State} state
* @returns {boolean}
*/
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.
* @function getLogOutError
* @function _getLogOutError
* @param {State} state
* @returns {string}
*/
@@ -215,7 +238,7 @@ export const _getLogOutError = (state: AuthState) => state.error;
/**
* Returns the sign up error.
* @function getRegistrationError
* @function _getRegistrationError
* @param {State} state
* @returns {string}
*/
@@ -223,7 +246,7 @@ export const _getRegistrationError = (state: AuthState) => state.error;
/**
* Returns the redirect url.
* @function getRedirectUrl
* @function _getRedirectUrl
* @param {State} state
* @returns {string}
*/

View File

@@ -11,12 +11,13 @@ import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
import { isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
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 { Store } from '@ngrx/store';
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
import { RouterReducerState } from '@ngrx/router-store';
import { Router } from '@angular/router';
import { CookieAttributes } from 'js-cookie';
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> {
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
* @returns {string}
*/
public getAuthHeader(): string {
const token = this.getToken();
public buildAuthHeader(token): string {
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
*
@@ -186,7 +230,9 @@ export class AuthService {
* @returns {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);
}
/**
* Replace authentication token info with a new one
*/
public replaceToken(token: AuthTokenInfo) {
this.removeToken();
return this.storeToken(token);
}
/**
* Redirect to the login route
*/

View File

@@ -1,13 +1,15 @@
import { default as decode } from 'jwt-decode';
export const TOKENITEM = 'dsAuthInfo';
export class AuthTokenInfo {
public accessToken: string;
public expires?: number;
public expires: number;
constructor(token: string, expiresIn?: number) {
constructor(token: string) {
this.accessToken = token.replace('Bearer ', '');
if (expiresIn) {
this.expires = expiresIn * 1000 + Date.now();
}
const tokenClaims = decode(this.accessToken);
// exp claim is in seconds, convert it se to milliseconds
this.expires = tokenClaims.exp * 1000;
}
}

View File

@@ -74,6 +74,15 @@ export const isAuthenticatedLoaded = createSelector(getAuthState, auth._isAuthen
*/
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.
* @function getLogOutError

View File

@@ -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 */

View File

@@ -236,7 +236,7 @@ describe('RequestService', () => {
it('should dispatch the request', () => {
service.configure(request);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request, false);
});
});
describe('and it is already cached or pending', () => {
@@ -254,22 +254,22 @@ describe('RequestService', () => {
describe('when the request isn\'t a GET request', () => {
it('should dispatch the request', () => {
service.configure(testPostRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest, false);
service.configure(testPutRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest, false);
service.configure(testDeleteRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest, false);
service.configure(testOptionsRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest, false);
service.configure(testHeadRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest, false);
service.configure(testPatchRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest, false);
});
});
});

View File

@@ -102,7 +102,7 @@ export class RequestService {
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 RequestExecuteAction(request.uuid));
if (request.method === RestRequestMethod.Get && !overrideRequest) {

View File

@@ -7,6 +7,6 @@ export interface DSpaceRESTV2Response {
_links?: any;
page?: any;
},
headers: HttpHeaders,
headers?: HttpHeaders,
statusCode: string
}

View File

@@ -71,6 +71,7 @@ export class DSpaceRESTv2Service {
console.log(res);
return ({ payload: res.body, headers: res.headers, statusCode: res.statusText })
})
.share()
.catch((err) => {
console.log('Error: ', err);
return Observable.throw(err);

View File

@@ -9,6 +9,16 @@ import { Observable } from 'rxjs/Observable';
import { HeaderComponent } from './header.component';
import { HeaderState } from './header.reducer';
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 fixture: ComponentFixture<HeaderComponent>;
@@ -19,8 +29,17 @@ describe('HeaderComponent', () => {
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [StoreModule.forRoot({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot()],
declarations: [HeaderComponent]
imports: [
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
}));

View File

@@ -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>
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
<input id="inputEmail"

View File

@@ -1,18 +1,13 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// @ngrx
import { Store } from '@ngrx/store';
// rxjs
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/takeWhile';
// actions
import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../core/auth/auth.actions';
// reducers
import {
getAuthenticationError, getAuthenticationInfo,
isAuthenticated,

View File

@@ -16,4 +16,8 @@ export class HostWindowServiceStub {
isXs(): Observable<boolean> {
return Observable.of(this.width < 576);
}
isMobileView(): Observable<boolean> {
return this.isXs();
}
}

View File

@@ -0,0 +1,12 @@
// declare a stub service
export class PlatformServiceStub {
public get isBrowser(): boolean {
return true;
}
public get isServer(): boolean {
return false;
}
}

View File

@@ -193,7 +193,7 @@
version "2.8.4"
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"
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"
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:
version "2.2.0"
resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf"