diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9098f7ca51..4ce1df54b0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -17,6 +17,9 @@ import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/host-window.reducer'; import { NativeWindowRef, NativeWindowService } from './shared/services/window.service'; +import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; +import { isAuthenticated } from './core/auth/selectors'; +import { PlatformService } from './shared/services/platform.service'; @Component({ selector: 'ds-app', @@ -32,7 +35,8 @@ export class AppComponent implements OnInit { @Inject(NativeWindowService) private _window: NativeWindowRef, private translate: TranslateService, private store: Store, - private metadata: MetadataService + private metadata: MetadataService, + private platformService: PlatformService ) { // this language will be used as a fallback when a translation isn't found in the current language translate.setDefaultLang('en'); @@ -51,6 +55,12 @@ export class AppComponent implements OnInit { const color: string = this.config.production ? 'red' : 'green'; console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight); + if (this.platformService.isServer) { + this.store.select(isAuthenticated) + .take(1) + .filter((authenticated) => !authenticated) + .subscribe((authenticated) => this.store.dispatch(new CheckAuthenticationTokenAction())); + } } @HostListener('window:resize', ['$event']) diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 6202b6784d..f54aa9f0be 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -31,6 +31,7 @@ export class AuthRequestService extends HALEndpointService { protected fetchRequest(request: RestRequest): Observable { const [successResponse, errorResponse] = this.responseCache.get(request.href) .map((entry: ResponseCacheEntry) => entry.response) + .do(() => this.responseCache.remove(request.href)) .partition((response: RestResponse) => response.isSuccessful); return Observable.merge( errorResponse.flatMap((response: ErrorResponse) => diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 1075cfe059..0215335f03 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -36,10 +36,10 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) { const response = this.process(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') { + } 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); diff --git a/src/app/core/auth/auth-storage.service.ts b/src/app/core/auth/auth-storage.service.ts deleted file mode 100644 index 01e41d850b..0000000000 --- a/src/app/core/auth/auth-storage.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; -import { isPlatformBrowser } from '@angular/common'; - -/** - * The auth service. - */ -@Injectable() -export class AuthStorageService { - - constructor(@Inject(PLATFORM_ID) private platformId: string) {} - - public get(key: string): any { - let item = null; - if (isPlatformBrowser(this.platformId)) { - item = JSON.parse(localStorage.getItem(key)); - } - return item; - } - - public store(key: string, item: any) { - if (isPlatformBrowser(this.platformId)) { - localStorage.setItem(key, JSON.stringify(item)); - } - return true; - } - - public remove(key: string) { - if (isPlatformBrowser(this.platformId)) { - localStorage.removeItem(key); - } - return true; - } - -} diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index cf8ce68bd6..0528607d55 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -15,6 +15,8 @@ export const AuthActionTypes = { AUTHENTICATED: type('dspace/auth/AUTHENTICATED'), AUTHENTICATED_ERROR: type('dspace/auth/AUTHENTICATED_ERROR'), AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'), + CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'), + CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'), RESET_ERROR: type('dspace/auth/RESET_ERROR'), LOG_OUT: type('dspace/auth/LOG_OUT'), LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'), @@ -116,6 +118,24 @@ export class AuthenticationSuccessAction implements Action { } } +/** + * Check if token is already present upon initial load. + * @class CheckAuthenticationTokenAction + * @implements {Action} + */ +export class CheckAuthenticationTokenAction implements Action { + public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN; +} + +/** + * Check Authentication Token Error. + * @class CheckAuthenticationTokenErrorAction + * @implements {Action} + */ +export class CheckAuthenticationTokenErrorAction implements Action { + public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR; +} + /** * Reset error. * @class ResetAuthenticationErrorAction @@ -215,6 +235,8 @@ export type AuthActions | AuthenticatedSuccessAction | AuthenticationErrorAction | AuthenticationSuccessAction + | CheckAuthenticationTokenAction + | CheckAuthenticationTokenErrorAction | RegistrationAction | RegistrationErrorAction | RegistrationSuccessAction; diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index aa7503fe4c..3d1bd43854 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; // import @ngrx import { Effect, Actions } from '@ngrx/effects'; -import { Action } from '@ngrx/store'; +import { Action, Store } from '@ngrx/store'; // import rxjs import { Observable } from 'rxjs/Observable'; @@ -16,7 +16,7 @@ import { AuthenticatedErrorAction, AuthenticatedSuccessAction, AuthenticationErrorAction, - AuthenticationSuccessAction, LogOutAction, + AuthenticationSuccessAction, CheckAuthenticationTokenAction, CheckAuthenticationTokenErrorAction, LogOutAction, LogOutErrorAction, LogOutSuccessAction, RegistrationAction, RegistrationErrorAction, @@ -24,6 +24,10 @@ import { } from './auth.actions'; import { Eperson } from '../eperson/models/eperson.model'; import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; +import { AppState } from '../../app.reducer'; +import { isAuthenticated } from './selectors'; +import { StoreActionTypes } from '../../store.actions'; @Injectable() export class AuthEffects { @@ -46,12 +50,7 @@ export class AuthEffects { public authenticateSuccess: Observable = this.actions$ .ofType(AuthActionTypes.AUTHENTICATE_SUCCESS) .do((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)) - .map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) - - @Effect({dispatch: false}) - public logOutSuccess: Observable = this.actions$ - .ofType(AuthActionTypes.LOG_OUT_SUCCESS) - .do((action: LogOutSuccessAction) => this.authService.removeToken()); + .map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)); @Effect() public authenticated: Observable = this.actions$ @@ -62,6 +61,20 @@ export class AuthEffects { .catch((error) => Observable.of(new AuthenticatedErrorAction(error))); }); + @Effect({dispatch: false}) + public authenticatedError: Observable = this.actions$ + .ofType(AuthActionTypes.AUTHENTICATED_ERROR) + .do((action: LogOutSuccessAction) => this.authService.removeToken()); + + @Effect() + public checkToken: Observable = this.actions$ + .ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN) + .switchMap(() => { + return this.authService.checkAuthenticationToken() + .map((token: AuthTokenInfo) => new AuthenticatedAction(token)) + .catch((error) => Observable.of(new CheckAuthenticationTokenErrorAction())); + }); + @Effect() public createUser: Observable = this.actions$ .ofType(AuthActionTypes.REGISTRATION) @@ -72,21 +85,41 @@ export class AuthEffects { .catch((error) => Observable.of(new RegistrationErrorAction(error))); }); + /** + * When the store is rehydrated in the browser, + * clear a possible invalid token + */ + @Effect({dispatch: false}) + public clearInvalidTokenOnRehydrate = this.actions$ + .ofType(StoreActionTypes.REHYDRATE) + .switchMap(() => { + return this.store.select(isAuthenticated) + .take(1) + .filter((authenticated) => !authenticated) + .do(() => this.authService.removeToken()); + }); + @Effect() - public signOut: Observable = this.actions$ + public logOut: Observable = this.actions$ .ofType(AuthActionTypes.LOG_OUT) - .switchMap((action: LogOutAction) => { - return this.authService.signout() + .switchMap(() => { + return this.authService.logout() .map((value) => new LogOutSuccessAction()) .catch((error) => Observable.of(new LogOutErrorAction(error))); }); + @Effect({dispatch: false}) + public logOutSuccess: Observable = this.actions$ + .ofType(AuthActionTypes.LOG_OUT_SUCCESS) + .do((action: LogOutSuccessAction) => this.authService.removeToken()); + /** * @constructor * @param {Actions} actions$ * @param {AuthService} authService */ constructor(private actions$: Actions, - private authService: AuthService) { + private authService: AuthService, + private store: Store) { } } diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 40fd0d3836..e8406566bc 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -23,6 +23,10 @@ export class AuthInterceptor implements HttpInterceptor { return status === 401 || status === 403; } + private isAuthRequest(url: string): boolean { + return url.endsWith('/authn/login') || url.endsWith('/authn/logout') || url.endsWith('/authn/status'); + } + private isLoginResponse(url: string): boolean { return url.endsWith('/authn/login'); } @@ -40,7 +44,7 @@ export class AuthInterceptor implements HttpInterceptor { authStatus.token = new AuthTokenInfo(accessToken); } else { authStatus.authenticated = false; - authStatus.error = JSON.parse(error); + authStatus.error = isNotEmpty(error) ? JSON.parse(error) : null; } return authStatus; } @@ -53,11 +57,11 @@ export class AuthInterceptor implements HttpInterceptor { const Authorization = authService.getAuthHeader(); let authReq; - if (isNotEmpty(Authorization)) { + if (!this.isAuthRequest(req.url) && isNotEmpty(Authorization)) { // Clone the request to add the new header. authReq = req.clone({headers: req.headers.set('authorization', Authorization)}); } else { - authReq = req.clone(); + authReq = req; } // Pass on the cloned request instead of the original request. @@ -67,6 +71,7 @@ export class AuthInterceptor implements HttpInterceptor { let authRes: HttpResponse; 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)}); } else { authRes = response.clone({body: this.makeAuthStatusObject(false)}); diff --git a/src/app/core/auth/auth.reducers.ts b/src/app/core/auth/auth.reducers.ts index 645541aa8e..31079767e5 100644 --- a/src/app/core/auth/auth.reducers.ts +++ b/src/app/core/auth/auth.reducers.ts @@ -6,7 +6,6 @@ import { // import models import { Eperson } from '../eperson/models/eperson.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; /** * The auth state. @@ -26,9 +25,6 @@ export interface AuthState { // true when loading loading: boolean; - // access token - token?: AuthTokenInfo; - // the authenticated user user?: Eperson; } @@ -61,7 +57,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { authenticated: false, error: (action as AuthenticationErrorAction).payload.message, - loaded: true + loaded: true, + loading: false }); case AuthActionTypes.AUTHENTICATED_SUCCESS: @@ -82,15 +79,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut }); case AuthActionTypes.AUTHENTICATE_SUCCESS: - const token: AuthTokenInfo = (action as AuthenticationSuccessAction).payload; - - // verify token is not null - if (token === null) { - return state; - } + return state; + case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN: return Object.assign({}, state, { - token: token + loading: true + }); + + case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR: + return Object.assign({}, state, { + loading: false }); case AuthActionTypes.REGISTRATION_SUCCESS: @@ -117,8 +115,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut error: undefined, loaded: false, loading: false, - user: undefined, - token: undefined + user: undefined }); case AuthActionTypes.REGISTRATION: diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index a5373e1afd..62f5aa5ba7 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -7,34 +7,9 @@ import { AuthRequestService } from './auth-request.service'; import { HttpHeaders } from '@angular/common/http'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; +import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; import { isNotEmpty, isNotNull } from '../../shared/empty.util'; -import { AuthStorageService } from './auth-storage.service'; - -export const MOCK_USER = new Eperson(); -MOCK_USER.id = '92a59227-ccf7-46da-9776-86c3fc64147f'; -MOCK_USER.uuid = '92a59227-ccf7-46da-9776-86c3fc64147f'; -MOCK_USER.name = 'andrea.bollini@4science.it'; -MOCK_USER.email = 'andrea.bollini@4science.it'; -MOCK_USER.metadata = [ - { - key: 'eperson.firstname', - value: 'Andrea', - language: null - }, - { - key: 'eperson.lastname', - value: 'Bollini', - language: null - }, - { - key: 'eperson.language', - value: 'en', - language: null - } -]; - -export const TOKENITEM = 'dsAuthInfo'; +import { CookieService } from '../../shared/services/cookie.service'; /** * The auth service. @@ -54,7 +29,7 @@ export class AuthService { */ private _redirectUrl: string; - constructor(private authRequestService: AuthRequestService, private storage: AuthStorageService) { + constructor(private authRequestService: AuthRequestService, private storage: CookieService) { } /** @@ -76,7 +51,7 @@ export class AuthService { let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); options.headers = headers; - options.responseType = 'text'; + // options.responseType = 'text'; return this.authRequestService.postToEndpoint('login', body, options) .map((status: AuthStatus) => { if (status.authenticated) { @@ -101,9 +76,7 @@ export class AuthService { * @returns {User} */ public authenticatedUser(token: AuthTokenInfo): Observable { - // Normally you would do an HTTP request to determine if - // the user has an existing auth session on the server - // but, let's just return the mock user for this example. + // Determine if the user has an existing auth session on the server const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Accept', 'application/json'); @@ -121,6 +94,14 @@ export class AuthService { }); } + /** + * Checks if token is present into storage + */ + public checkAuthenticationToken(): Observable { + const token = this.getToken(); + return isNotEmpty(token) ? Observable.of(token) : Observable.throw(false); + } + /** * Create a new user * @returns {User} @@ -137,7 +118,7 @@ export class AuthService { * End session * @returns {Observable} */ - public signout(): Observable { + public logout(): Observable { // Normally you would do an HTTP request sign end the session // but, let's just return an observable of true. let headers = new HttpHeaders(); @@ -168,11 +149,12 @@ export class AuthService { public storeToken(token: AuthTokenInfo) { // Save authentication token info - return this.storage.store(TOKENITEM, JSON.stringify(token)); + return this.storage.set(TOKENITEM, token); } public removeToken() { // Remove authentication token info + console.log('REMOVE!!!!'); return this.storage.remove(TOKENITEM); } diff --git a/src/app/core/auth/models/auth-token-info.model.ts b/src/app/core/auth/models/auth-token-info.model.ts index 1e798f1630..e6f6afc872 100644 --- a/src/app/core/auth/models/auth-token-info.model.ts +++ b/src/app/core/auth/models/auth-token-info.model.ts @@ -1,3 +1,5 @@ +export const TOKENITEM = 'dsAuthInfo'; + export class AuthTokenInfo { public accessToken: string; public expires?: number; diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts index 77a2402043..a0e3740094 100644 --- a/src/app/core/cache/response-cache.service.ts +++ b/src/app/core/cache/response-cache.service.ts @@ -65,6 +65,11 @@ export class ResponseCacheService { return result; } + remove(key: string): void { + if (this.has(key)) { + this.store.dispatch(new ResponseCacheRemoveAction(key)); + } + } /** * Check whether a ResponseCacheEntry should still be cached * diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 638aa4c4a5..9c57026975 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -44,7 +44,8 @@ import { AuthRequestService } from './auth/auth-request.service'; import { AuthResponseParsingService } from './auth/auth-response-parsing.service'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth/auth.interceptor'; -import { AuthStorageService } from './auth/auth-storage.service'; +import { CookieService } from '../shared/services/cookie.service'; +import { PlatformService } from '../shared/services/platform.service'; const IMPORTS = [ CommonModule, @@ -66,9 +67,9 @@ const PROVIDERS = [ AuthRequestService, AuthResponseParsingService, AuthService, - AuthStorageService, CommunityDataService, CollectionDataService, + CookieService, DSOResponseParsingService, DSpaceRESTv2Service, HostWindowService, @@ -76,6 +77,7 @@ const PROVIDERS = [ MetadataService, ObjectCacheService, PaginationComponentOptions, + PlatformService, RemoteDataBuildService, RequestService, ResponseCacheService, @@ -90,7 +92,7 @@ const PROVIDERS = [ SubmissionSectionsConfigService, UUIDService, { provide: NativeWindowService, useFactory: NativeWindowFactory }, - // register TokenInterceptor as HttpInterceptor + // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index 453f68ea86..b9e46c6159 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -35,18 +35,17 @@ export class AuthNavMenuComponent implements OnDestroy, OnInit { protected subs: Subscription[] = []; constructor( - private appStore: Store, - private coreStore: Store, + private store: Store, public windowService: HostWindowService) { } ngOnInit(): void { // set loading - this.isAuthenticated = this.coreStore.select(isAuthenticated); + this.isAuthenticated = this.store.select(isAuthenticated); - this.user = this.appStore.select(getAuthenticatedUser); + this.user = this.store.select(getAuthenticatedUser); - this.subs.push(this.appStore.select(routerStateSelector) + this.subs.push(this.store.select(routerStateSelector) .filter((router: RouterReducerState) => isNotUndefined(router)) .subscribe((router: RouterReducerState) => { this.showAuth = router.state.url !== '/login'; diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index df77dad74f..ffaa30e29e 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -1,8 +1,9 @@ import { HttpClient, HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; +import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; +import { REQUEST } from '@nguniversal/express-engine/tokens'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; @@ -15,10 +16,16 @@ import { AppModule } from '../../app/app.module'; import { DSpaceBrowserTransferStateModule } from '../transfer-state/dspace-browser-transfer-state.module'; import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; +export const REQ_KEY = makeStateKey('req'); + export function createTranslateLoader(http: HttpClient) { return new TranslateHttpLoader(http, 'assets/i18n/', '.json'); } +export function getRequest(transferState: TransferState): any { + return transferState.get(REQ_KEY, {}) +} + @NgModule({ bootstrap: [AppComponent], imports: [ @@ -45,6 +52,13 @@ export function createTranslateLoader(http: HttpClient) { }), AppModule ], + providers: [ + { + provide: REQUEST, + useFactory: getRequest, + deps: [TransferState] + } + ] }) export class BrowserAppModule { constructor(