Removed use of platform service and created different service for SSR and CSR

This commit is contained in:
Giuseppe Digilio
2018-05-03 19:08:07 +02:00
parent 3b9a334258
commit c8a1fe0860
14 changed files with 234 additions and 106 deletions

View File

@@ -1,22 +1,13 @@
import {
async,
ComponentFixture,
inject,
TestBed
} from '@angular/core/testing';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import {
CUSTOM_ELEMENTS_SCHEMA,
DebugElement
} from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { CommonModule } from '@angular/common';
import { By } from '@angular/platform-browser';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Store, StoreModule } from '@ngrx/store';
// Load the implementations that should be tested
import { AppComponent } from './app.component';
@@ -25,13 +16,11 @@ import { HostWindowResizeAction } from './shared/host-window.actions';
import { MetadataService } from './core/metadata/metadata.service';
import { GLOBAL_CONFIG, ENV_CONFIG } from '../config';
import { ENV_CONFIG, GLOBAL_CONFIG } from '../config';
import { NativeWindowRef, NativeWindowService } from './shared/services/window.service';
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>;
@@ -58,7 +47,6 @@ 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

@@ -1,11 +1,4 @@
import {
ChangeDetectionStrategy,
Component,
HostListener,
Inject,
OnInit,
ViewEncapsulation
} from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
@@ -17,9 +10,8 @@ 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';
import { AuthService } from './core/auth/auth.service';
@Component({
selector: 'ds-app',
@@ -36,7 +28,7 @@ export class AppComponent implements OnInit {
private translate: TranslateService,
private store: Store<HostWindowState>,
private metadata: MetadataService,
private platformService: PlatformService
private authService: AuthService
) {
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en');
@@ -55,12 +47,13 @@ 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) {
// Whether is not authenticathed try to retrieve a possible stored auth token
this.store.select(isAuthenticated)
.take(1)
.filter((authenticated) => !authenticated)
.subscribe((authenticated) => this.store.dispatch(new CheckAuthenticationTokenAction()));
}
.subscribe((authenticated) => this.authService.checksAuthenticationToken());
}
@HostListener('window:resize', ['$event'])

View File

@@ -71,7 +71,7 @@ export class AuthEffects {
public checkToken: Observable<Action> = this.actions$
.ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN)
.switchMap(() => {
return this.authService.checkAuthenticationToken()
return this.authService.hasValidAuthenticationToken()
.map((token: AuthTokenInfo) => new AuthenticatedAction(token))
.catch((error) => Observable.of(new CheckAuthenticationTokenErrorAction()));
});

View File

@@ -15,11 +15,15 @@ import { CookieService } from '../../shared/services/cookie.service';
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
import { AppState, routerStateSelector } from '../../app.reducer';
import { Store } from '@ngrx/store';
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
import {
CheckAuthenticationTokenAction,
ResetAuthenticationMessagesAction,
SetRedirectUrlAction
} from './auth.actions';
import { RouterReducerState } from '@ngrx/router-store';
import { CookieAttributes } from 'js-cookie';
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
import { PlatformService } from '../../shared/services/platform.service';
import { REQUEST } from '@nguniversal/express-engine/tokens';
export const LOGIN_ROUTE = '/login';
@@ -35,14 +39,14 @@ export class AuthService {
* True if authenticated
* @type boolean
*/
private _authenticated: boolean;
protected _authenticated: boolean;
constructor(@Inject(NativeWindowService) private _window: NativeWindowRef,
private authRequestService: AuthRequestService,
private platform: PlatformService,
private router: Router,
private storage: CookieService,
private store: Store<AppState>) {
constructor(@Inject(REQUEST) protected req: any,
@Inject(NativeWindowService) protected _window: NativeWindowRef,
protected authRequestService: AuthRequestService,
protected router: Router,
protected storage: CookieService,
protected store: Store<AppState>) {
this.store.select(isAuthenticated)
.startWith(false)
.subscribe((authenticated: boolean) => this._authenticated = authenticated);
@@ -130,10 +134,17 @@ export class AuthService {
});
}
/**
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
*/
public checksAuthenticationToken() {
return
}
/**
* Checks if token is present into storage and is not expired
*/
public checkAuthenticationToken(): Observable<AuthTokenInfo> {
public hasValidAuthenticationToken(): Observable<AuthTokenInfo> {
return this.store.select(getAuthenticationToken)
.take(1)
.map((authTokenInfo: AuthTokenInfo) => {
@@ -317,10 +328,9 @@ export class AuthService {
this.getRedirectUrl()
.first()
.subscribe((redirectUrl) => {
console.log('Browser');
if (isNotEmpty(redirectUrl)) {
if (this.platform.isBrowser) {
this.clearRedirectUrl();
}
// override the route reuse strategy
this.router.routeReuseStrategy.shouldReuseRoute = () => {

View File

@@ -0,0 +1,77 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpHeaders } from '@angular/common/http';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from './models/auth-status.model';
import { isNotEmpty } from '../../shared/empty.util';
import { AuthService } from './auth.service';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { CheckAuthenticationTokenAction } from './auth.actions';
/**
* The auth service.
*/
@Injectable()
export class ServerAuthService extends AuthService {
/**
* Authenticate the user
*
* @param {string} user The user name
* @param {string} password The user's password
* @returns {Observable<User>} The authenticated user observable.
*/
public authenticate(user: string, password: string): Observable<AuthStatus> {
// Attempt authenticating the user using the supplied credentials.
const body = encodeURI(`password=${password}&user=${user}`);
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
// NB this could be use to avoid the problem with the authentication is case the UI is rendered by Angular Universal.
const clientIp = this.req.connection.remoteAddress;
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
options.headers = headers;
return this.authRequestService.postToEndpoint('login', body, options)
.map((status: AuthStatus) => {
if (status.authenticated) {
return status;
} else {
throw(new Error('Invalid email or password'));
}
})
}
/**
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
*/
public checksAuthenticationToken() {
this.store.dispatch(new CheckAuthenticationTokenAction())
}
/**
* Redirect to the route navigated before the login
*/
public redirectToPreviousUrl() {
this.getRedirectUrl()
.first()
.subscribe((redirectUrl) => {
console.log('server side');
if (isNotEmpty(redirectUrl)) {
// override the route reuse strategy
this.router.routeReuseStrategy.shouldReuseRoute = () => {
return false;
};
this.router.navigated = false;
const url = decodeURIComponent(redirectUrl);
this.router.navigateByUrl(url);
} else {
this.router.navigate(['/']);
}
})
}
}

View File

@@ -38,13 +38,11 @@ import { SubmissionDefinitionsConfigService } from './config/submission-definiti
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
import { UUIDService } from './shared/uuid.service';
import { AuthService } from './auth/auth.service';
import { AuthenticatedGuard } from './auth/authenticated.guard';
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 { CookieService } from '../shared/services/cookie.service';
import { PlatformService } from '../shared/services/platform.service';
const IMPORTS = [
@@ -66,10 +64,8 @@ const PROVIDERS = [
AuthenticatedGuard,
AuthRequestService,
AuthResponseParsingService,
AuthService,
CommunityDataService,
CollectionDataService,
CookieService,
DSOResponseParsingService,
DSpaceRESTv2Service,
HostWindowService,

View File

@@ -1,5 +1,5 @@
<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>
<ds-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-loading>
<form *ngIf="!(loading | async) && !(isAuthenticated | 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"
autocomplete="off"
@@ -12,12 +12,12 @@
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
<input id="inputPassword"
autocomplete="off"
class="form-control form-control-lg position-relative"
class="form-control form-control-lg position-relative mb-3"
placeholder="{{'login.form.password' | translate}}"
formControlName="password"
required
type="password">
<div *ngIf="platform.isBrowser && (error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ (error | async) | translate }}</div>
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ (error | async) | translate }}</div>
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert" @fadeOut>{{ (message | async) | translate }}</div>
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [disabled]="!form.valid">{{"login.form.submit" | translate}}</button>
<div class="dropdown-divider"></div>

View File

@@ -9,7 +9,8 @@ import 'rxjs/add/operator/takeWhile';
import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../core/auth/auth.actions';
import {
getAuthenticationError, getAuthenticationInfo,
getAuthenticationError,
getAuthenticationInfo,
isAuthenticated,
isAuthenticationLoading,
} from '../../core/auth/selectors';
@@ -18,7 +19,6 @@ import { CoreState } from '../../core/core.reducers';
import { isNotEmpty } from '../empty.util';
import { fadeOut } from '../animations/fade';
import { AuthService } from '../../core/auth/auth.service';
import { PlatformService } from '../services/platform.service';
/**
* /users/sign-in
@@ -56,6 +56,12 @@ export class LogInComponent implements OnDestroy, OnInit {
*/
public hasMessage = false;
/**
* Whether user is authenticated.
* @type {Observable<string>}
*/
public isAuthenticated: Observable<boolean>;
/**
* True if the authentication is loading.
* @type {boolean}
@@ -83,15 +89,18 @@ export class LogInComponent implements OnDestroy, OnInit {
constructor(
private authService: AuthService,
private formBuilder: FormBuilder,
private platform: PlatformService,
private store: Store<CoreState>
) { }
) {
}
/**
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
* @method ngOnInit
*/
public ngOnInit() {
// set isAuthenticated
this.isAuthenticated = this.store.select(isAuthenticated);
// set formGroup
this.form = this.formBuilder.group({
email: ['', Validators.required],
@@ -144,7 +153,7 @@ export class LogInComponent implements OnDestroy, OnInit {
}
/**
* To to the registration page.
* To the registration page.
* @method register
*/
public register() {

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core'
import { CookieAttributes, getJSON, remove, set } from 'js-cookie'
import { CookieService, ICookieService } from './cookie.service';
@Injectable()
export class ClientCookieService extends CookieService implements ICookieService {
public set(name: string, value: any, options?: CookieAttributes): void {
set(name, value, options);
this.updateSource()
}
public remove(name: string, options?: CookieAttributes): void {
remove(name, options);
this.updateSource()
}
public get(name: string): any {
return getJSON(name)
}
public getAll(): any {
return getJSON()
}
}

View File

@@ -1,4 +1,3 @@
import { PlatformService } from './platform.service'
import { CookieService, ICookieService } from './cookie.service'
import { async, TestBed } from '@angular/core/testing'
import { REQUEST } from '@nguniversal/express-engine/tokens'
@@ -10,7 +9,6 @@ describe(CookieService.name, () => {
TestBed.configureTestingModule({
providers: [
CookieService,
PlatformService,
{provide: REQUEST, useValue: {}}
]
})

View File

@@ -1,62 +1,40 @@
import { REQUEST } from '@nguniversal/express-engine/tokens'
import { PlatformService } from './platform.service'
import { Inject, Injectable } from '@angular/core'
import { REQUEST } from '@nguniversal/express-engine/tokens'
import { Subject } from 'rxjs/Subject'
import { Observable } from 'rxjs/Observable'
import { CookieAttributes, getJSON, remove, set } from 'js-cookie'
import { CookieAttributes } from 'js-cookie'
export interface ICookieService {
readonly cookies$: Observable<{ readonly [key: string]: any }>
getAll(): any
get(name: string): any
set(name: string, value: any, options?: CookieAttributes): void
remove(name: string, options?: CookieAttributes): void
}
@Injectable()
export class CookieService implements ICookieService {
private readonly cookieSource = new Subject<{ readonly [key: string]: any }>();
export abstract class CookieService implements ICookieService {
protected readonly cookieSource = new Subject<{ readonly [key: string]: any }>();
public readonly cookies$ = this.cookieSource.asObservable();
constructor(private platformService: PlatformService, @Inject(REQUEST) private req: any) { }
public set(name: string, value: any, options?: CookieAttributes): void {
if (this.platformService.isBrowser) {
set(name, value, options);
this.updateSource()
}
constructor(@Inject(REQUEST) protected req: any) {
}
public remove(name: string, options?: CookieAttributes): void {
if (this.platformService.isBrowser) {
remove(name, options);
this.updateSource()
}
}
public abstract set(name: string, value: any, options?: CookieAttributes): void
public get(name: string): any {
if (this.platformService.isBrowser) {
return getJSON(name)
} else {
try {
return JSON.parse(this.req.cookies[name])
} catch (err) {
return this.req ? this.req.cookies[name] : undefined
}
}
}
public abstract remove(name: string, options?: CookieAttributes): void
public getAll(): any {
if (this.platformService.isBrowser) {
return getJSON()
} else {
if (this.req) {
return this.req.cookies
}
}
}
public abstract get(name: string): any
private updateSource() {
this.cookieSource.next(this.getAll())
public abstract getAll(): any
protected updateSource() {
this.cookieSource.next(this.getAll());
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core'
import { CookieAttributes } from 'js-cookie'
import { CookieService, ICookieService } from './cookie.service';
@Injectable()
export class ServerCookieService extends CookieService implements ICookieService {
public set(name: string, value: any, options?: CookieAttributes): void {
return
}
public remove(name: string, options?: CookieAttributes): void {
return
}
public get(name: string): any {
console.log(this.req.connection.remoteAddress);
try {
return JSON.parse(this.req.cookies[name])
} catch (err) {
return this.req ? this.req.cookies[name] : undefined
}
}
public getAll(): any {
if (this.req) {
return this.req.cookies
}
}
}

View File

@@ -15,6 +15,10 @@ import { AppComponent } from '../../app/app.component';
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';
import { ClientCookieService } from '../../app/shared/services/client-cookie.service';
import { CookieService } from '../../app/shared/services/cookie.service';
import { ServerAuthService } from '../../app/core/auth/server-auth.service';
import { AuthService } from '../../app/core/auth/auth.service';
export const REQ_KEY = makeStateKey<string>('req');
@@ -57,6 +61,14 @@ export function getRequest(transferState: TransferState): any {
provide: REQUEST,
useFactory: getRequest,
deps: [TransferState]
},
{
provide: AuthService,
useClass: AuthService
},
{
provide: CookieService,
useClass: ClientCookieService
}
]
})

View File

@@ -15,6 +15,10 @@ import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
import { TranslateUniversalLoader } from '../translate-universal-loader';
import { CookieService } from '../../app/shared/services/cookie.service';
import { ServerCookieService } from '../../app/shared/services/server-cookie.service';
import { AuthService } from '../../app/core/auth/auth.service';
import { ServerAuthService } from '../../app/core/auth/server-auth.service';
export function createTranslateLoader() {
return new TranslateUniversalLoader('dist/assets/i18n/', '.json');
@@ -42,6 +46,14 @@ export function createTranslateLoader() {
AppModule
],
providers: [
{
provide: AuthService,
useClass: ServerAuthService
},
{
provide: CookieService,
useClass: ServerCookieService
}
]
})
export class ServerAppModule {