diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index f93c0e933e..c148b06565 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -47,7 +47,8 @@
},
"nav": {
"home": "Home",
- "login": "Log In"
+ "login": "Log In",
+ "logout": "Log Out"
},
"pagination": {
"results-per-page": "Results Per Page",
@@ -150,10 +151,19 @@
"login": {
"title": "Login",
"form": {
- "header": "Please log in",
+ "header": "Please log in to DSpace",
"email": "Email address",
+ "forgot-password": "Have you forgotten your password?",
+ "new-user": "New user? Click here to register.",
"password": "Password",
"submit": "Log in"
}
+ },
+ "logout": {
+ "title": "Logout",
+ "form": {
+ "header": "Log out from DSpace",
+ "submit": "Log out"
+ }
}
}
diff --git a/src/app/+home-page/home-page.component.html b/src/app/+home-page/home-page.component.html
index 6a3e20ca9d..974ca86f44 100644
--- a/src/app/+home-page/home-page.component.html
+++ b/src/app/+home-page/home-page.component.html
@@ -1,5 +1,6 @@
diff --git a/src/app/+home-page/home-page.component.ts b/src/app/+home-page/home-page.component.ts
index ad25ec0155..7736d874d9 100644
--- a/src/app/+home-page/home-page.component.ts
+++ b/src/app/+home-page/home-page.component.ts
@@ -1,10 +1,21 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import { isAuthenticated } from '../core/auth/selectors';
+import { Observable } from 'rxjs/Observable';
+import { AppState } from '../app.reducer';
+import { Store } from '@ngrx/store';
@Component({
selector: 'ds-home-page',
styleUrls: ['./home-page.component.scss'],
templateUrl: './home-page.component.html'
})
-export class HomePageComponent {
+export class HomePageComponent implements OnInit {
+ public isAuthenticated: Observable;
+
+ constructor(private store: Store) {}
+ ngOnInit() {
+ // set loading
+ this.isAuthenticated = this.store.select(isAuthenticated);
+ }
}
diff --git a/src/app/+login-page/login-page.component.html b/src/app/+login-page/login-page.component.html
index 5a37df9d2a..76f4e68ef1 100644
--- a/src/app/+login-page/login-page.component.html
+++ b/src/app/+login-page/login-page.component.html
@@ -1,5 +1,5 @@
{{"login.form.header" | translate}}
-
+
diff --git a/src/app/+logout-page/logout-page-routing.module.ts b/src/app/+logout-page/logout-page-routing.module.ts
new file mode 100644
index 0000000000..64894c1f87
--- /dev/null
+++ b/src/app/+logout-page/logout-page-routing.module.ts
@@ -0,0 +1,19 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { LogoutPageComponent } from './logout-page.component';
+import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ {
+ canActivate: [AuthenticatedGuard],
+ path: '',
+ component: LogoutPageComponent,
+ data: { title: 'logout.title' }
+ }
+ ])
+ ]
+})
+export class LogoutPageRoutingModule { }
diff --git a/src/app/+logout-page/logout-page.component.html b/src/app/+logout-page/logout-page.component.html
new file mode 100644
index 0000000000..e427d78ff7
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.html
@@ -0,0 +1,5 @@
+
+

+
{{"logout.form.header" | translate}}
+
+
diff --git a/src/app/+logout-page/logout-page.component.scss b/src/app/+logout-page/logout-page.component.scss
new file mode 100644
index 0000000000..b71eee7ee1
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.scss
@@ -0,0 +1,15 @@
+.login-container {
+ height: 100%;
+ display: -ms-flexbox;
+ display: -webkit-box;
+ display: flex;
+ -ms-flex-align: center;
+ -ms-flex-pack: center;
+ -webkit-box-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ justify-content: center;
+ padding-top: 40px;
+ padding-bottom: 40px;
+ background-color: #f5f5f5;
+}
diff --git a/src/app/+logout-page/logout-page.component.ts b/src/app/+logout-page/logout-page.component.ts
new file mode 100644
index 0000000000..4fa4b9900a
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'ds-logout-page',
+ styleUrls: ['./logout-page.component.scss'],
+ templateUrl: './logout-page.component.html'
+})
+export class LogoutPageComponent {
+
+}
diff --git a/src/app/+logout-page/logout-page.module.ts b/src/app/+logout-page/logout-page.module.ts
new file mode 100644
index 0000000000..b085a5117b
--- /dev/null
+++ b/src/app/+logout-page/logout-page.module.ts
@@ -0,0 +1,19 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { LogoutPageComponent } from './logout-page.component';
+import { LogoutPageRoutingModule } from './logout-page-routing.module';
+
+@NgModule({
+ imports: [
+ LogoutPageRoutingModule,
+ CommonModule,
+ SharedModule,
+ ],
+ declarations: [
+ LogoutPageComponent
+ ]
+})
+export class LogoutPageModule {
+
+}
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index c11881800e..72a26fd678 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -13,6 +13,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
+ { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
])
],
diff --git a/src/app/app.effects.ts b/src/app/app.effects.ts
index 7fc42da80d..5cd9c79e30 100644
--- a/src/app/app.effects.ts
+++ b/src/app/app.effects.ts
@@ -1,6 +1,7 @@
import { HeaderEffects } from './header/header.effects';
import { StoreEffects } from './store.effects';
+import { AuthEffects } from './core/auth/auth.effects';
export const appEffects = [
StoreEffects,
diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts
index 72b29519de..e69f29ae52 100644
--- a/src/app/app.reducer.ts
+++ b/src/app/app.reducer.ts
@@ -1,29 +1,30 @@
-import { ActionReducerMap } from '@ngrx/store';
-import * as fromRouter from '@ngrx/router-store';
-
-import { headerReducer, HeaderState } from './header/header.reducer';
-import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
-import {
- SearchSidebarState,
- sidebarReducer
-} from './+search-page/search-sidebar/search-sidebar.reducer';
-import {
- filterReducer,
- SearchFiltersState
-} from './+search-page/search-filters/search-filter/search-filter.reducer';
-
-export interface AppState {
- router: fromRouter.RouterReducerState;
- hostWindow: HostWindowState;
- header: HeaderState;
- searchSidebar: SearchSidebarState;
- searchFilter: SearchFiltersState;
-}
-
-export const appReducers: ActionReducerMap = {
- router: fromRouter.routerReducer,
- hostWindow: hostWindowReducer,
- header: headerReducer,
- searchSidebar: sidebarReducer,
- searchFilter: filterReducer
-};
+import { ActionReducerMap } from '@ngrx/store';
+import * as fromRouter from '@ngrx/router-store';
+
+import { headerReducer, HeaderState } from './header/header.reducer';
+import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
+import {
+ SearchSidebarState,
+ sidebarReducer
+} from './+search-page/search-sidebar/search-sidebar.reducer';
+import {
+ filterReducer,
+ SearchFiltersState
+} from './+search-page/search-filters/search-filter/search-filter.reducer';
+import { authReducer, AuthState } from './core/auth/auth.reducers';
+
+export interface AppState {
+ router: fromRouter.RouterReducerState;
+ hostWindow: HostWindowState;
+ header: HeaderState;
+ searchSidebar: SearchSidebarState;
+ searchFilter: SearchFiltersState;
+}
+
+export const appReducers: ActionReducerMap = {
+ router: fromRouter.routerReducer,
+ hostWindow: hostWindowReducer,
+ header: headerReducer,
+ searchSidebar: sidebarReducer,
+ searchFilter: filterReducer
+};
diff --git a/src/app/core/auth/auth-object-factory.ts b/src/app/core/auth/auth-object-factory.ts
new file mode 100644
index 0000000000..e1960e8111
--- /dev/null
+++ b/src/app/core/auth/auth-object-factory.ts
@@ -0,0 +1,19 @@
+
+import { AuthType } from './auth-type';
+import { AuthStatus } from './models/auth-status.model';
+import { GenericConstructor } from '../shared/generic-constructor';
+import { DSpaceObject } from '../shared/dspace-object.model';
+
+export class AuthObjectFactory {
+ public static getConstructor(type): GenericConstructor {
+ switch (type) {
+ case AuthType.Status: {
+ return AuthStatus
+ }
+
+ default: {
+ return undefined;
+ }
+ }
+ }
+}
diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts
new file mode 100644
index 0000000000..24bdc16135
--- /dev/null
+++ b/src/app/core/auth/auth-request.service.ts
@@ -0,0 +1,58 @@
+import { Inject, Injectable } from '@angular/core';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { ResponseCacheService } from '../cache/response-cache.service';
+import { RequestService } from '../data/request.service';
+import { GLOBAL_CONFIG } from '../../../config';
+import { GlobalConfig } from '../../../config/global-config.interface';
+import { Observable } from 'rxjs/Observable';
+import { isNotEmpty } from '../../shared/empty.util';
+import { AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
+import { ResponseCacheEntry } from '../cache/response-cache.reducer';
+import { AuthSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
+import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
+
+@Injectable()
+export class AuthRequestService extends HALEndpointService {
+ protected linkName = 'authn';
+ protected browseEndpoint = '';
+
+ /**
+ * True if authenticated
+ * @type
+ */
+ private _authenticated = false;
+
+ constructor(protected responseCache: ResponseCacheService,
+ protected requestService: RequestService,
+ @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
+ super();
+ }
+
+ protected submitRequest(request: RestRequest): Observable {
+ const [successResponse, errorResponse] = this.responseCache.get(request.href)
+ .map((entry: ResponseCacheEntry) => entry.response)
+ .partition((response: RestResponse) => response.isSuccessful);
+ return Observable.merge(
+ errorResponse.flatMap((response: ErrorResponse) =>
+ Observable.throw(new Error(`Couldn't send data to server`))),
+ successResponse
+ .filter((response: AuthSuccessResponse) => isNotEmpty(response))
+ .map((response: AuthSuccessResponse) => response.authResponse)
+ .distinctUntilChanged());
+ }
+
+ protected getEndpointByMethod(endpoint: string, method: string): string {
+ return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
+ }
+
+ public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable {
+ return this.getEndpoint()
+ .filter((href: string) => isNotEmpty(href))
+ .map((endpointURL) => this.getEndpointByMethod(endpointURL, method))
+ .distinctUntilChanged()
+ .map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options))
+ .do((request: PostRequest) => this.requestService.configure(request))
+ .flatMap((request: PostRequest) => this.submitRequest(request))
+ .distinctUntilChanged();
+ }
+}
diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts
new file mode 100644
index 0000000000..21a4ae94eb
--- /dev/null
+++ b/src/app/core/auth/auth-response-parsing.service.ts
@@ -0,0 +1,47 @@
+import { Inject, Injectable } from '@angular/core';
+
+import { AuthObjectFactory } from './auth-object-factory';
+import { BaseResponseParsingService } from '../data/base-response-parsing.service';
+import {
+ AuthSuccessResponse, ConfigSuccessResponse, ErrorResponse,
+ 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 { isNotEmpty } from '../../shared/empty.util';
+import { ObjectCacheService } from '../cache/object-cache.service';
+import { ResponseParsingService } from '../data/parsing.service';
+import { RestRequest } from '../data/request.models';
+
+@Injectable()
+export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
+
+ protected objectFactory = AuthObjectFactory;
+ protected toCache = false;
+
+ 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') {
+ const configDefinition = this.process(data.payload, request.href);
+ return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page));
+ } else {
+ return new ErrorResponse(
+ Object.assign(
+ new Error('Unexpected response from config endpoint'),
+ {statusText: data.statusCode}
+ )
+ );
+ }*/
+ console.log(data);
+ return new AuthSuccessResponse(data.payload, data.statusCode)
+ }
+
+}
diff --git a/src/app/core/auth/auth-type.ts b/src/app/core/auth/auth-type.ts
new file mode 100644
index 0000000000..793c9869a0
--- /dev/null
+++ b/src/app/core/auth/auth-type.ts
@@ -0,0 +1,3 @@
+export enum AuthType {
+ Status = 'status'
+}
diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts
new file mode 100644
index 0000000000..694d59b9ee
--- /dev/null
+++ b/src/app/core/auth/auth.actions.ts
@@ -0,0 +1,219 @@
+// import @ngrx
+import { Action } from '@ngrx/store';
+
+// import type function
+import { type } from '../../shared/ngrx/type';
+
+// import models
+import { Eperson } from '../eperson/models/eperson.model';
+
+export const AuthActionTypes = {
+ AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
+ AUTHENTICATE_ERROR: type('dspace/auth/AUTHENTICATE_ERROR'),
+ AUTHENTICATE_SUCCESS: type('dspace/auth/AUTHENTICATE_SUCCESS'),
+ AUTHENTICATED: type('dspace/auth/AUTHENTICATED'),
+ AUTHENTICATED_ERROR: type('dspace/auth/AUTHENTICATED_ERROR'),
+ AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
+ RESET_ERROR: type('dspace/auth/RESET_ERROR'),
+ LOG_OUT: type('dspace/auth/LOG_OUT'),
+ LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'),
+ LOG_OUT_SUCCESS: type('dspace/auth/LOG_OUT_SUCCESS'),
+ REGISTRATION: type('dspace/auth/REGISTRATION'),
+ REGISTRATION_ERROR: type('dspace/auth/REGISTRATION_ERROR'),
+ REGISTRATION_SUCCESS: type('dspace/auth/REGISTRATION_SUCCESS')
+};
+
+/* tslint:disable:max-classes-per-file */
+
+/**
+ * Authenticate.
+ * @class AuthenticateAction
+ * @implements {Action}
+ */
+export class AuthenticateAction implements Action {
+ public type: string = AuthActionTypes.AUTHENTICATE;
+ payload: {
+ email: string;
+ password: string
+ };
+
+ constructor(email: string, password: string) {
+ this.payload = { email, password };
+ }
+}
+
+/**
+ * Checks if user is authenticated.
+ * @class AuthenticatedAction
+ * @implements {Action}
+ */
+export class AuthenticatedAction implements Action {
+ public type: string = AuthActionTypes.AUTHENTICATED;
+ payload: string;
+
+ constructor(token: string) {
+ this.payload = token;
+ }
+}
+
+/**
+ * Authenticated check success.
+ * @class AuthenticatedSuccessAction
+ * @implements {Action}
+ */
+export class AuthenticatedSuccessAction implements Action {
+ public type: string = AuthActionTypes.AUTHENTICATED_SUCCESS;
+ payload: {
+ authenticated: boolean;
+ user: Eperson
+ };
+
+ constructor(authenticated: boolean, user: Eperson) {
+ this.payload = { authenticated, user };
+ }
+}
+
+/**
+ * Authenticated check error.
+ * @class AuthenticatedErrorAction
+ * @implements {Action}
+ */
+export class AuthenticatedErrorAction implements Action {
+ public type: string = AuthActionTypes.AUTHENTICATED_ERROR;
+ payload: Error;
+
+ constructor(payload: Error) {
+ this.payload = payload ;
+ }
+}
+
+/**
+ * Authentication error.
+ * @class AuthenticationErrorAction
+ * @implements {Action}
+ */
+export class AuthenticationErrorAction implements Action {
+ public type: string = AuthActionTypes.AUTHENTICATE_ERROR;
+ payload: Error;
+
+ constructor(payload: Error) {
+ this.payload = payload ;
+ }
+}
+
+/**
+ * Authentication success.
+ * @class AuthenticationSuccessAction
+ * @implements {Action}
+ */
+export class AuthenticationSuccessAction implements Action {
+ public type: string = AuthActionTypes.AUTHENTICATE_SUCCESS;
+ payload: Eperson;
+
+ constructor(user: Eperson) {
+ this.payload = user;
+ }
+}
+
+/**
+ * Reset error.
+ * @class ResetAuthenticationErrorAction
+ * @implements {Action}
+ */
+export class ResetAuthenticationErrorAction implements Action {
+ public type: string = AuthActionTypes.RESET_ERROR;
+}
+
+/**
+ * Sign out.
+ * @class LogOutAction
+ * @implements {Action}
+ */
+export class LogOutAction implements Action {
+ public type: string = AuthActionTypes.LOG_OUT;
+ constructor(public payload?: any) {}
+}
+
+/**
+ * Sign out error.
+ * @class LogOutErrorAction
+ * @implements {Action}
+ */
+export class LogOutErrorAction implements Action {
+ public type: string = AuthActionTypes.LOG_OUT_ERROR;
+ payload: Error;
+
+ constructor(payload: Error) {
+ this.payload = payload ;
+ }
+}
+
+/**
+ * Sign out success.
+ * @class LogOutSuccessAction
+ * @implements {Action}
+ */
+export class LogOutSuccessAction implements Action {
+ public type: string = AuthActionTypes.LOG_OUT_SUCCESS;
+ constructor(public payload?: any) {}
+}
+
+/**
+ * Sign up.
+ * @class RegistrationAction
+ * @implements {Action}
+ */
+export class RegistrationAction implements Action {
+ public type: string = AuthActionTypes.REGISTRATION;
+ payload: Eperson;
+
+ constructor(user: Eperson) {
+ this.payload = user;
+ }
+}
+
+/**
+ * Sign up error.
+ * @class RegistrationErrorAction
+ * @implements {Action}
+ */
+export class RegistrationErrorAction implements Action {
+ public type: string = AuthActionTypes.REGISTRATION_ERROR;
+ payload: Error;
+
+ constructor(payload: Error) {
+ this.payload = payload ;
+ }
+}
+
+/**
+ * Sign up success.
+ * @class RegistrationSuccessAction
+ * @implements {Action}
+ */
+export class RegistrationSuccessAction implements Action {
+ public type: string = AuthActionTypes.REGISTRATION_SUCCESS;
+ payload: Eperson;
+
+ constructor(user: Eperson) {
+ this.payload = user;
+ }
+}
+
+/* tslint:enable:max-classes-per-file */
+
+/**
+ * Actions type.
+ * @type {AuthActions}
+ */
+export type AuthActions
+ =
+ AuthenticateAction
+ | AuthenticatedAction
+ | AuthenticatedErrorAction
+ | AuthenticatedSuccessAction
+ | AuthenticationErrorAction
+ | AuthenticationSuccessAction
+ | RegistrationAction
+ | RegistrationErrorAction
+ | RegistrationSuccessAction;
diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts
new file mode 100644
index 0000000000..75ca333b5a
--- /dev/null
+++ b/src/app/core/auth/auth.effects.ts
@@ -0,0 +1,97 @@
+import { Injectable } from '@angular/core';
+
+// import @ngrx
+import { Effect, Actions } from '@ngrx/effects';
+import { Action } from '@ngrx/store';
+
+// import rxjs
+import { Observable } from 'rxjs/Observable';
+
+// import services
+import { AuthService } from './auth.service';
+
+// import actions
+import {
+ AuthActionTypes, AuthenticateAction, AuthenticatedAction,
+ AuthenticatedErrorAction,
+ AuthenticatedSuccessAction,
+ AuthenticationErrorAction,
+ AuthenticationSuccessAction, LogOutAction,
+ LogOutErrorAction,
+ LogOutSuccessAction, RegistrationAction,
+ RegistrationErrorAction,
+ RegistrationSuccessAction
+} from './auth.actions';
+import { Eperson } from '../eperson/models/eperson.model';
+
+/**
+ * Effects offer a way to isolate and easily test side-effects within your
+ * application.
+ * The `toPayload` helper function returns just
+ * the payload of the currently dispatched action, useful in
+ * instances where the current state is not necessary.
+ *
+ * Documentation on `toPayload` can be found here:
+ * https://github.com/ngrx/effects/blob/master/docs/api.md#topayload
+ *
+ * If you are unfamiliar with the operators being used in these examples, please
+ * check out the sources below:
+ *
+ * Official Docs: http://reactivex.io/rxjs/manual/overview.html#categories-of-operators
+ * RxJS 5 Operators By Example: https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35
+ */
+
+@Injectable()
+export class AuthEffects {
+
+ /**
+ * Authenticate user.
+ * @method authenticate
+ */
+ @Effect()
+ public authenticate: Observable = this.actions$
+ .ofType(AuthActionTypes.AUTHENTICATE)
+ .debounceTime(500)
+ .switchMap((action: AuthenticateAction) => {
+ return this.authService.authenticate(action.payload.email, action.payload.password)
+ .map((user: Eperson) => new AuthenticationSuccessAction(user))
+ .catch((error) => Observable.of(new AuthenticationErrorAction(error)));
+ });
+
+ @Effect()
+ public authenticated: Observable = this.actions$
+ .ofType(AuthActionTypes.AUTHENTICATED)
+ .switchMap((action: AuthenticatedAction) => {
+ return this.authService.authenticatedUser()
+ .map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), user))
+ .catch((error) => Observable.of(new AuthenticatedErrorAction(error)));
+ });
+
+ @Effect()
+ public createUser: Observable = this.actions$
+ .ofType(AuthActionTypes.REGISTRATION)
+ .debounceTime(500)
+ .switchMap((action: RegistrationAction) => {
+ return this.authService.create(action.payload)
+ .map((user: Eperson) => new RegistrationSuccessAction(user))
+ .catch((error) => Observable.of(new RegistrationErrorAction(error)));
+ });
+
+ @Effect()
+ public signOut: Observable = this.actions$
+ .ofType(AuthActionTypes.LOG_OUT)
+ .switchMap((action: LogOutAction) => {
+ return this.authService.signout()
+ .map((value) => new LogOutSuccessAction())
+ .catch((error) => Observable.of(new LogOutErrorAction(error)));
+ });
+
+ /**
+ * @constructor
+ * @param {Actions} actions$
+ * @param {AuthService} authService
+ */
+ constructor(private actions$: Actions,
+ private authService: AuthService) {
+ }
+}
diff --git a/src/app/core/auth/auth.reducers.ts b/src/app/core/auth/auth.reducers.ts
new file mode 100644
index 0000000000..293e7b1527
--- /dev/null
+++ b/src/app/core/auth/auth.reducers.ts
@@ -0,0 +1,181 @@
+// import actions
+import {
+ AuthActions, AuthActionTypes, AuthenticatedSuccessAction, AuthenticationErrorAction,
+ AuthenticationSuccessAction, LogOutErrorAction
+} from './auth.actions';
+
+// import models
+import { Eperson } from '../eperson/models/eperson.model';
+
+/**
+ * The auth state.
+ * @interface State
+ */
+export interface AuthState {
+
+ // boolean if user is authenticated
+ authenticated: boolean;
+
+ // error message
+ error?: string;
+
+ // true if we have attempted existing auth session
+ loaded: boolean;
+
+ // true when loading
+ loading: boolean;
+
+ // the authenticated user
+ user?: Eperson;
+}
+
+/**
+ * The initial state.
+ */
+const initialState: AuthState = {
+ authenticated: null,
+ loaded: false,
+ loading: false
+};
+
+/**
+ * The reducer function.
+ * @function reducer
+ * @param {State} state Current state
+ * @param {AuthActions} action Incoming action
+ */
+export function authReducer(state: any = initialState, action: AuthActions): AuthState {
+
+ switch (action.type) {
+ case AuthActionTypes.AUTHENTICATE:
+ return Object.assign({}, state, {
+ error: undefined,
+ loading: true
+ });
+
+ case AuthActionTypes.AUTHENTICATED_ERROR:
+ return Object.assign({}, state, {
+ authenticated: false,
+ error: (action as AuthenticationErrorAction).payload.message,
+ loaded: true
+ });
+
+ case AuthActionTypes.AUTHENTICATED_SUCCESS:
+ return Object.assign({}, state, {
+ authenticated: (action as AuthenticatedSuccessAction).payload.authenticated,
+ loaded: true,
+ user: (action as AuthenticatedSuccessAction).payload.user
+ });
+
+ case AuthActionTypes.AUTHENTICATE_ERROR:
+ case AuthActionTypes.REGISTRATION_ERROR:
+ return Object.assign({}, state, {
+ authenticated: false,
+ error: (action as AuthenticationErrorAction).payload.message,
+ loading: false
+ });
+
+ case AuthActionTypes.AUTHENTICATE_SUCCESS:
+ case AuthActionTypes.REGISTRATION_SUCCESS:
+ const user: Eperson = (action as AuthenticationSuccessAction).payload;
+
+ // verify user is not null
+ if (user === null) {
+ return state;
+ }
+
+ return Object.assign({}, state, {
+ authenticated: true,
+ error: undefined,
+ loading: false,
+ user: user
+ });
+
+ case AuthActionTypes.RESET_ERROR:
+ return Object.assign({}, state, {
+ authenticated: null,
+ loaded: false,
+ loading: false
+ });
+
+ case AuthActionTypes.LOG_OUT_ERROR:
+ return Object.assign({}, state, {
+ authenticated: true,
+ error: (action as LogOutErrorAction).payload.message,
+ user: undefined
+ });
+
+ case AuthActionTypes.LOG_OUT_SUCCESS:
+ return Object.assign({}, state, {
+ authenticated: false,
+ error: undefined,
+ user: undefined
+ });
+
+ case AuthActionTypes.REGISTRATION:
+ return Object.assign({}, state, {
+ authenticated: false,
+ error: undefined,
+ loading: true
+ });
+
+ default:
+ return state;
+ }
+}
+
+/**
+ * Returns true if the user is authenticated.
+ * @function isAuthenticated
+ * @param {State} state
+ * @returns {boolean}
+ */
+export const isAuthenticated = (state: AuthState) => state.authenticated;
+
+/**
+ * Returns true if the authenticated has loaded.
+ * @function isAuthenticatedLoaded
+ * @param {State} state
+ * @returns {boolean}
+ */
+export const isAuthenticatedLoaded = (state: AuthState) => state.loaded;
+
+/**
+ * Return the users state
+ * @function getAuthenticatedUser
+ * @param {State} state
+ * @returns {User}
+ */
+export const getAuthenticatedUser = (state: AuthState) => state.user;
+
+/**
+ * Returns the authentication error.
+ * @function getAuthenticationError
+ * @param {State} state
+ * @returns {Error}
+ */
+export const getAuthenticationError = (state: AuthState) => state.error;
+
+/**
+ * Returns true if request is in progress.
+ * @function isLoading
+ * @param {State} state
+ * @returns {boolean}
+ */
+export const isLoading = (state: AuthState) => state.loading;
+
+/**
+ * Returns the sign out error.
+ * @function getLogOutError
+ * @param {State} state
+ * @returns {Error}
+ */
+export const getLogOutError = (state: AuthState) => state.error;
+
+/**
+ * Returns the sign up error.
+ * @function getRegistrationError
+ * @param {State} state
+ * @returns {Error}
+ */
+export const getRegistrationError = (state: AuthState) => state.error;
diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts
index 2ca53fd160..c59c5d069e 100644
--- a/src/app/core/auth/auth.service.ts
+++ b/src/app/core/auth/auth.service.ts
@@ -3,6 +3,9 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Eperson } from '../eperson/models/eperson.model';
+import { AuthRequestService } from './auth-request.service';
+import { HttpHeaders } from '@angular/common/http';
+import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
export const MOCK_USER = new Eperson();
MOCK_USER.id = '92a59227-ccf7-46da-9776-86c3fc64147f';
@@ -27,6 +30,8 @@ MOCK_USER.metadata = [
}
];
+export const TOKENITEM = 'ds-token';
+
/**
* The user service.
*/
@@ -39,18 +44,36 @@ export class AuthService {
*/
private _authenticated = false;
+ constructor(private authRequestService: AuthRequestService) {}
+
/**
* Authenticate the user
*
- * @param {string} email The user's email address
+ * @param {string} user The user name
* @param {string} password The user's password
* @returns {Observable} The authenticated user observable.
*/
- public authenticate(email: string, password: string): Observable {
+ public authenticate(user: string, password: string): Observable {
// Normally you would do an HTTP request to determine to
// attempt authenticating the user using the supplied credentials.
-
- if (email === MOCK_USER.email && password === 'password') {
+ // const body = `user=${user}&password=${password}`;
+ // const body = encodeURI('password=test&user=vera.aloe@mailinator.com');
+ // const body = [{user}, {password}];
+ const formData: FormData = new FormData();
+ formData.append('user', user);
+ formData.append('password', password);
+ const body = 'password=' + password.toString() + '&user=' + user.toString();
+ const options: HttpOptions = Object.create({});
+ let headers = new HttpHeaders();
+ headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
+ headers = headers.append('Accept', 'application/json');
+ options.headers = headers;
+ options.responseType = 'text';
+ this.authRequestService.postToEndpoint('login', body, options)
+ .subscribe((r) => {
+ console.log(r);
+ })
+ if (user === 'test' && password === 'password') {
this._authenticated = true;
return Observable.of(MOCK_USER);
}
diff --git a/src/app/core/auth/authenticated.guard.ts b/src/app/core/auth/authenticated.guard.ts
index 3cbecf1215..38e0b5e24d 100644
--- a/src/app/core/auth/authenticated.guard.ts
+++ b/src/app/core/auth/authenticated.guard.ts
@@ -5,10 +5,8 @@ import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
// reducers
-import {
- isAuthenticated,
- State
-} from '../app.reducers';
+import { CoreState } from '../core.reducers';
+import { isAuthenticated } from './selectors';
/**
* Prevent unauthorized activating and loading of routes
@@ -20,7 +18,7 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
/**
* @constructor
*/
- constructor(private router: Router, private store: Store) {}
+ constructor(private router: Router, private store: Store) {}
/**
* True when user is authenticated
diff --git a/src/app/core/auth/authentication-status.model.ts b/src/app/core/auth/authentication-status.model.ts
deleted file mode 100644
index 9ef027d600..0000000000
--- a/src/app/core/auth/authentication-status.model.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { DSpaceObject } from '../shared/dspace-object.model';
-
-export class AuthenticationStatus extends DSpaceObject {
-
- okay: boolean;
-
- authenticated: boolean;
-
-}
diff --git a/src/app/core/auth/models/auth-info.model.ts b/src/app/core/auth/models/auth-info.model.ts
new file mode 100644
index 0000000000..8995f82235
--- /dev/null
+++ b/src/app/core/auth/models/auth-info.model.ts
@@ -0,0 +1,5 @@
+export interface AuthInfo {
+ access_token?: string,
+ expires?: number,
+ expires_in?: number
+}
diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts
new file mode 100644
index 0000000000..388405e87a
--- /dev/null
+++ b/src/app/core/auth/models/auth-status.model.ts
@@ -0,0 +1,9 @@
+import { DSpaceObject } from '../../shared/dspace-object.model';
+
+export class AuthStatus extends DSpaceObject {
+
+ okay: boolean;
+
+ authenticated: boolean;
+
+}
diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts
new file mode 100644
index 0000000000..a260f84762
--- /dev/null
+++ b/src/app/core/auth/selectors.ts
@@ -0,0 +1,84 @@
+import { createSelector } from '@ngrx/store';
+
+import { AuthState } from './auth.reducers';
+import { coreSelector, CoreState } from '../core.reducers';
+
+/**
+ * Every reducer module's default export is the reducer function itself. In
+ * addition, each module should export a type or interface that describes
+ * the state of the reducer plus any selector functions. The `* as`
+ * notation packages up all of the exports into a single object.
+ */
+import * as auth from './auth.reducers';
+import { AppState } from '../../app.reducer';
+
+/**
+ * Returns the user state.
+ * @function getUserState
+ * @param {AppState} state Top level state.
+ * @return {AuthState}
+ */
+export const getAuthState = (state: any) => state.core.auth;
+
+/**
+ * Returns the authenticated user
+ * @function getAuthenticatedUser
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {User}
+ */
+export const getAuthenticatedUser = createSelector(getAuthState, auth.getAuthenticatedUser);
+
+/**
+ * Returns the authentication error.
+ * @function getAuthenticationError
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {Error}
+ */
+export const getAuthenticationError = createSelector(getAuthState, auth.getAuthenticationError);
+
+/**
+ * Returns true if the user is authenticated
+ * @function isAuthenticated
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {boolean}
+ */
+export const isAuthenticated = createSelector(getAuthState, auth.isAuthenticated);
+
+/**
+ * Returns true if the user is authenticated
+ * @function isAuthenticated
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {boolean}
+ */
+export const isAuthenticatedLoaded = createSelector(getAuthState, auth.isAuthenticatedLoaded);
+
+/**
+ * Returns true if the authentication request is loading.
+ * @function isAuthenticationLoading
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {boolean}
+ */
+export const isAuthenticationLoading = createSelector(getAuthState, auth.isLoading);
+
+/**
+ * Returns the log out error.
+ * @function getLogOutError
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {Error}
+ */
+export const getLogOutError = createSelector(getAuthState, auth.getLogOutError);
+
+/**
+ * Returns the registration error.
+ * @function getRegistrationError
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {Error}
+ */
+export const getRegistrationError = createSelector(getAuthState, auth.getRegistrationError);
diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts
index 06fc26aa67..0a61a53df7 100644
--- a/src/app/core/cache/response-cache.models.ts
+++ b/src/app/core/cache/response-cache.models.ts
@@ -62,4 +62,14 @@ export class ConfigSuccessResponse extends RestResponse {
super(true, statusCode);
}
}
+
+export class AuthSuccessResponse extends RestResponse {
+ constructor(
+ public authResponse: any,
+ public statusCode: string,
+ public pageInfo?: PageInfo
+ ) {
+ super(true, statusCode);
+ }
+}
/* tslint:enable:max-classes-per-file */
diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts
index 7cda10b4ae..279b80159f 100644
--- a/src/app/core/core.effects.ts
+++ b/src/app/core/core.effects.ts
@@ -3,10 +3,12 @@ import { ObjectCacheEffects } from './cache/object-cache.effects';
import { ResponseCacheEffects } from './cache/response-cache.effects';
import { UUIDIndexEffects } from './index/index.effects';
import { RequestEffects } from './data/request.effects';
+import { AuthEffects } from './auth/auth.effects';
export const coreEffects = [
ResponseCacheEffects,
RequestEffects,
ObjectCacheEffects,
UUIDIndexEffects,
+ AuthEffects,
];
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 768f05f24b..7ea4ef45d4 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -38,6 +38,9 @@ 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';
const IMPORTS = [
CommonModule,
@@ -55,6 +58,9 @@ const EXPORTS = [
const PROVIDERS = [
ApiService,
+ AuthenticatedGuard,
+ AuthRequestService,
+ AuthService,
CommunityDataService,
CollectionDataService,
DSOResponseParsingService,
diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts
index d2898eb3c3..37027465b4 100644
--- a/src/app/core/core.reducers.ts
+++ b/src/app/core/core.reducers.ts
@@ -4,19 +4,22 @@ import { responseCacheReducer, ResponseCacheState } from './cache/response-cache
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
import { indexReducer, IndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer';
+import { authReducer, AuthState } from './auth/auth.reducers';
export interface CoreState {
'data/object': ObjectCacheState,
'data/response': ResponseCacheState,
'data/request': RequestState,
- 'index': IndexState
+ 'index': IndexState,
+ 'auth': AuthState,
}
export const coreReducers: ActionReducerMap = {
'data/object': objectCacheReducer,
'data/response': responseCacheReducer,
'data/request': requestReducer,
- 'index': indexReducer
+ 'index': indexReducer,
+ 'auth': authReducer
};
export const coreSelector = createFeatureSelector('core');
diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts
index 379540c779..2f9d168f74 100644
--- a/src/app/core/data/request.effects.ts
+++ b/src/app/core/data/request.effects.ts
@@ -33,9 +33,9 @@ export class RequestEffects {
let body;
if (isNotEmpty(request.body)) {
const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type));
- body = JSON.stringify(serializer.serialize(request.body));
+ body = serializer.serialize(request.body);
}
- return this.restApi.request(request.method, request.href, body)
+ return this.restApi.request(request.method, request.href, body, request.options)
.map((data: DSpaceRESTV2Response) =>
this.injector.get(request.getResponseParser()).parse(request, data))
.do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))
diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts
index ee37f9c3d4..b7c81eaa8e 100644
--- a/src/app/core/data/request.models.ts
+++ b/src/app/core/data/request.models.ts
@@ -7,6 +7,9 @@ import { ResponseParsingService } from './parsing.service';
import { RootResponseParsingService } from './root-response-parsing.service';
import { BrowseResponseParsingService } from './browse-response-parsing.service';
import { ConfigResponseParsingService } from './config-response-parsing.service';
+import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
+import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
+import { HttpHeaders } from '@angular/common/http';
/* tslint:disable:max-classes-per-file */
@@ -34,7 +37,8 @@ export abstract class RestRequest {
public uuid: string,
public href: string,
public method: RestRequestMethod = RestRequestMethod.Get,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
}
@@ -47,7 +51,8 @@ export class GetRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.Get, body)
}
@@ -57,7 +62,8 @@ export class PostRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.Post, body)
}
@@ -67,7 +73,8 @@ export class PutRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.Put, body)
}
@@ -77,7 +84,8 @@ export class DeleteRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.Delete, body)
}
@@ -87,7 +95,8 @@ export class OptionsRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.Options, body)
}
@@ -97,7 +106,8 @@ export class HeadRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.Head, body)
}
@@ -107,7 +117,8 @@ export class PatchRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
- public body?: any
+ public body?: any,
+ public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.Patch, body)
}
@@ -134,7 +145,7 @@ export class FindAllRequest extends GetRequest {
constructor(
uuid: string,
href: string,
- public options?: FindAllOptions,
+ public body?: FindAllOptions,
) {
super(uuid, href);
}
@@ -171,6 +182,26 @@ export class ConfigRequest extends GetRequest {
}
}
+export class AuthPostRequest extends PostRequest {
+ constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
+ super(uuid, href, body, options);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return AuthResponseParsingService;
+ }
+}
+
+export class AuthGetRequest extends GetRequest {
+ constructor(uuid: string, href: string) {
+ super(uuid, href);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return AuthResponseParsingService;
+ }
+}
+
export class RequestError extends Error {
statusText: string;
}
diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
index b2d3197723..1a5287d4b0 100644
--- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
+++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
@@ -1,10 +1,21 @@
import { Injectable } from '@angular/core';
import { Request } from '@angular/http';
-import { HttpClient, HttpResponse } from '@angular/common/http'
+import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'
import { Observable } from 'rxjs/Observable';
import { RestRequestMethod } from '../data/request.models';
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
+import { HttpObserve } from '@angular/common/http/src/client';
+
+export interface HttpOptions {
+ body?: any;
+ headers?: HttpHeaders;
+ params?: HttpParams;
+ observe?: HttpObserve;
+ reportProgress?: boolean;
+ responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';
+ withCredentials?: boolean;
+}
/**
* Service to access DSpace's REST API
@@ -45,8 +56,25 @@ export class DSpaceRESTv2Service {
* @return {Observable}
* An Observable containing the response from the server
*/
- request(method: RestRequestMethod, url: string, body?: any): Observable {
- return this.http.request(method, url, { body, observe: 'response' })
+ request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable {
+ const requestOptions: HttpOptions = {};
+ requestOptions.body = body;
+ requestOptions.observe = 'response';
+ if (options && options.headers) {
+ let headers = new HttpHeaders();
+ headers = headers.append('Accept', 'application/json');
+ headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
+ // requestOptions.headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded');
+ requestOptions.headers = headers;
+ /* const keys = options.headers.getAll('');
+ keys.forEach((key) => {
+ requestOptions.headers.append(key, options.headers.get(key));
+ })*/
+ }
+ if (options && options.responseType) {
+ // requestOptions.responseType = options.responseType;
+ }
+ return this.http.request(method, url, requestOptions)
.map((res) => ({ payload: res.body, statusCode: res.statusText }))
.catch((err) => {
console.log('Error: ', err);
diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts
index 84587f1eea..109d4e84b3 100644
--- a/src/app/core/shared/hal-endpoint.service.ts
+++ b/src/app/core/shared/hal-endpoint.service.ts
@@ -23,9 +23,10 @@ export abstract class HALEndpointService {
.distinctUntilChanged();
}
- public getEndpoint(): Observable {
+ public getEndpoint(linkName?: string): Observable {
+ const mapLinkName = isNotEmpty(linkName) ? linkName : this.linkName;
return this.getEndpointMap()
- .map((map: EndpointMap) => map[this.linkName])
+ .map((map: EndpointMap) => map[mapLinkName])
.distinctUntilChanged();
}
diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html
index ac76ea1dab..2edfa8a796 100644
--- a/src/app/header/header.component.html
+++ b/src/app/header/header.component.html
@@ -12,19 +12,7 @@
{{ 'nav.home' | translate }}(current)
-
+
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index 4be64221dc..93cb329f4f 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -1,12 +1,12 @@
import { Component, OnInit } from '@angular/core';
import { createSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
+import { RouterReducerState } from '@ngrx/router-store';
import { HeaderState } from './header.reducer';
import { HeaderToggleAction } from './header.actions';
import { AppState } from '../app.reducer';
import { HostWindowService } from '../shared/host-window.service';
-import { fadeInOut } from '../shared/animations/fade';
const headerStateSelector = (state: AppState) => state.header;
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
@@ -15,12 +15,15 @@ const navCollapsedSelector = createSelector(headerStateSelector, (header: Header
selector: 'ds-header',
styleUrls: ['header.component.scss'],
templateUrl: 'header.component.html',
- animations: [
- fadeInOut
- ]
})
export class HeaderComponent implements OnInit {
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated: Observable;
public isNavBarCollapsed: Observable;
+ public showAuth = false;
constructor(
private store: Store,
@@ -29,6 +32,7 @@ export class HeaderComponent implements OnInit {
}
ngOnInit(): void {
+ // set loading
this.isNavBarCollapsed = this.store.select(navCollapsedSelector);
}
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
new file mode 100644
index 0000000000..34db6a2130
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
@@ -0,0 +1,26 @@
+
+
+
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss b/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss
new file mode 100644
index 0000000000..e69de29bb2
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
new file mode 100644
index 0000000000..e188498b0a
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
@@ -0,0 +1,61 @@
+
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { RouterReducerState } from '@ngrx/router-store';
+import { Store } from '@ngrx/store';
+
+import { fadeInOut, fadeOut } from '../animations/fade';
+import { CoreState } from '../../core/core.reducers';
+import { HostWindowService } from '../host-window.service';
+import { AppState } from '../../app.reducer';
+import { hasValue, isNotUndefined } from '../empty.util';
+import { getAuthenticatedUser, isAuthenticated } from '../../core/auth/selectors';
+import { Subscription } from 'rxjs/Subscription';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+
+const routerStateSelector = (state: AppState) => state.router;
+
+@Component({
+ selector: 'ds-auth-nav-menu',
+ templateUrl: './auth-nav-menu.component.html',
+ styleUrls: ['./auth-nav-menu.component.scss'],
+ animations: [fadeInOut, fadeOut]
+})
+export class AuthNavMenuComponent implements OnDestroy, OnInit {
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated: Observable;
+
+ public showAuth = false;
+
+ public user: Observable;
+
+ protected subs: Subscription[] = [];
+
+ constructor(
+ private appStore: Store,
+ private coreStore: Store,
+ private windowService: HostWindowService) {
+ }
+
+ ngOnInit(): void {
+ // set loading
+ this.isAuthenticated = this.coreStore.select(isAuthenticated);
+
+ this.user = this.appStore.select(getAuthenticatedUser);
+
+ this.subs.push(this.appStore.select(routerStateSelector)
+ .filter((router: RouterReducerState) => isNotUndefined(router))
+ .subscribe((router: RouterReducerState) => {
+ this.showAuth = router.state.url !== '/login';
+ }));
+ }
+
+ ngOnDestroy() {
+ this.subs
+ .filter((sub) => hasValue(sub))
+ .forEach((sub) => sub.unsubscribe());
+ }
+}
diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html
new file mode 100644
index 0000000000..92b7cb08bb
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.html
@@ -0,0 +1,29 @@
+
+
+
+
diff --git a/src/app/shared/login-form/login-form.component.scss b/src/app/shared/log-in/log-in.component.scss
similarity index 100%
rename from src/app/shared/login-form/login-form.component.scss
rename to src/app/shared/log-in/log-in.component.scss
diff --git a/src/app/shared/log-in/log-in.component.spec.ts b/src/app/shared/log-in/log-in.component.spec.ts
new file mode 100644
index 0000000000..76fd631c5b
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.spec.ts
@@ -0,0 +1,116 @@
+/* tslint:disable:no-unused-variable */
+import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from "@angular/core";
+import { ComponentFixture, TestBed, async } from "@angular/core/testing";
+import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
+import { MaterialModule } from "@angular/material";
+import { By } from "@angular/platform-browser";
+import { Store, StoreModule } from "@ngrx/store";
+import { go } from "@ngrx/router-store";
+
+// reducers
+import { reducer } from "../../app.reducers";
+
+// models
+import { User } from "../../core/models/user";
+
+// services
+import { MOCK_USER } from "../../core/services/user.service";
+
+// this component to test
+import { LogInComponent } from "./log-in.component";
+
+describe("LogInComponent", () => {
+
+ let component: LogInComponent;
+ let fixture: ComponentFixture;
+ let page: Page;
+ let user: User = new User();
+
+ beforeEach(() => {
+ user = MOCK_USER;
+ });
+
+ beforeEach(async(() => {
+ // refine the test module by declaring the test component
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MaterialModule,
+ ReactiveFormsModule,
+ StoreModule.provideStore(reducer)
+ ],
+ declarations: [
+ LogInComponent
+ ],
+ schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ })
+ .compileComponents();
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(LogInComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+ }));
+
+ beforeEach(() => {
+ // create page
+ page = new Page(component, fixture);
+
+ // verify the fixture is stable (no pending tasks)
+ fixture.whenStable().then(() => {
+ page.addPageElements();
+ });
+ });
+
+ it("should create a FormGroup comprised of FormControls", () => {
+ fixture.detectChanges();
+ expect(component.form instanceof FormGroup).toBe(true);
+ });
+
+ it("should authenticate", () => {
+ fixture.detectChanges();
+
+ // set FormControl values
+ component.form.controls["email"].setValue(user.email);
+ component.form.controls["password"].setValue(user.password);
+
+ // submit form
+ component.submit();
+
+ // verify Store.dispatch() is invoked
+ expect(page.navigateSpy.calls.any()).toBe(true, "Store.dispatch not invoked");
+ });
+});
+
+/**
+ * I represent the DOM elements and attach spies.
+ *
+ * @class Page
+ */
+class Page {
+
+ public emailInput: HTMLInputElement;
+ public navigateSpy: jasmine.Spy;
+ public passwordInput: HTMLInputElement;
+
+ constructor(private component: LogInComponent, private fixture: ComponentFixture) {
+ // use injector to get services
+ const injector = fixture.debugElement.injector;
+ const store = injector.get(Store);
+
+ // add spies
+ this.navigateSpy = spyOn(store, "dispatch");
+ }
+
+ public addPageElements() {
+ const emailInputSelector = "input[formcontrolname=\"email\"]";
+ // console.log(this.fixture.debugElement.query(By.css(emailInputSelector)));
+ this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement;
+
+ const passwordInputSelector = "input[formcontrolname=\"password\"]";
+ this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement;
+ }
+}
diff --git a/src/app/shared/log-in/log-in.component.ts b/src/app/shared/log-in/log-in.component.ts
new file mode 100644
index 0000000000..faf17eedd5
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.ts
@@ -0,0 +1,163 @@
+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, ResetAuthenticationErrorAction } from '../../core/auth/auth.actions';
+
+// reducers
+import {
+ getAuthenticationError,
+ isAuthenticated,
+ isAuthenticationLoading,
+} from '../../core/auth/selectors';
+import { Router } from '@angular/router';
+import { CoreState } from '../../core/core.reducers';
+
+import { isNotEmpty, isNotNull } from '../empty.util';
+import { fadeOut } from '../animations/fade';
+
+/**
+ * /users/sign-in
+ * @class LogInComponent
+ */
+@Component({
+ selector: 'ds-log-in',
+ templateUrl: './log-in.component.html',
+ styleUrls: ['./log-in.component.scss'],
+ animations: [fadeOut]
+})
+export class LogInComponent implements OnDestroy, OnInit {
+
+ /**
+ * The error if authentication fails.
+ * @type {Observable}
+ */
+ public error: Observable;
+
+ /**
+ * Has authentication error.
+ * @type {boolean}
+ */
+ public hasError = false;
+
+ /**
+ * True if the authentication is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ /**
+ * The authentication form.
+ * @type {FormGroup}
+ */
+ public form: FormGroup;
+
+ /**
+ * Component state.
+ * @type {boolean}
+ */
+ private alive = true;
+
+ /**
+ * @constructor
+ * @param {FormBuilder} formBuilder
+ * @param {Store} store
+ */
+ constructor(
+ private formBuilder: FormBuilder,
+ private router: Router,
+ private store: Store
+ ) { }
+
+ /**
+ * Lifecycle hook that is called after data-bound properties of a directive are initialized.
+ * @method ngOnInit
+ */
+ public ngOnInit() {
+ // set formGroup
+ this.form = this.formBuilder.group({
+ email: ['', Validators.required],
+ password: ['', Validators.required]
+ });
+
+ // set error
+ this.error = this.store.select(getAuthenticationError)
+ .map((error) => {
+ this.hasError = (isNotEmpty(error));
+ return error;
+ });
+
+ // set loading
+ this.loading = this.store.select(isAuthenticationLoading);
+
+ // subscribe to success
+ this.store.select(isAuthenticated)
+ .takeWhile(() => this.alive)
+ .filter((authenticated) => authenticated)
+ .subscribe(() => {
+ this.router.navigate(['/']);
+ });
+ }
+
+ /**
+ * Lifecycle hook that is called when a directive, pipe or service is destroyed.
+ * @method ngOnDestroy
+ */
+ public ngOnDestroy() {
+ this.alive = false;
+ }
+
+ /**
+ * Go to the home page.
+ * @method home
+ */
+ public home() {
+ this.router.navigate(['/home']);
+ }
+
+ /**
+ * Reset error.
+ */
+ public resetError() {
+ if (this.hasError) {
+ this.store.dispatch(new ResetAuthenticationErrorAction());
+ this.hasError = false;
+ }
+ }
+
+ /**
+ * To to the registration page.
+ * @method register
+ */
+ public register() {
+ this.router.navigate(['/register']);
+ }
+
+ /**
+ * Submit the authentication form.
+ * @method submit
+ */
+ public submit() {
+ // get email and password values
+ const email: string = this.form.get('email').value;
+ const password: string = this.form.get('password').value;
+
+ // trim values
+ email.trim();
+ password.trim();
+
+ // dispatch AuthenticationAction
+ this.store.dispatch(new AuthenticateAction(email, password));
+
+ // clear form
+ this.form.reset();
+ }
+}
diff --git a/src/app/shared/log-out/log-out.component.html b/src/app/shared/log-out/log-out.component.html
new file mode 100644
index 0000000000..6e4ee32f6f
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.html
@@ -0,0 +1,9 @@
+
+
diff --git a/src/app/shared/log-out/log-out.component.scss b/src/app/shared/log-out/log-out.component.scss
new file mode 100644
index 0000000000..dcd67e092f
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.scss
@@ -0,0 +1 @@
+@import '../log-in/log-in.component.scss';
diff --git a/src/app/shared/log-out/log-out.component.spec.ts b/src/app/shared/log-out/log-out.component.spec.ts
new file mode 100644
index 0000000000..4beb53aadc
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.spec.ts
@@ -0,0 +1,45 @@
+/* tslint:disable:no-unused-variable */
+import { DebugElement, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
+import { async, ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
+import { Router } from "@angular/router";
+
+// import ngrx
+import { Store, StoreModule } from "@ngrx/store";
+
+// reducers
+import { reducer } from "../../app.reducers";
+
+// test this component
+import { SignOutComponent } from "./log-out.component";
+
+describe("Component: Signout", () => {
+ let component: SignOutComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ // refine the test module by declaring the test component
+ TestBed.configureTestingModule({
+ imports: [
+ StoreModule.provideStore(reducer)
+ ],
+ declarations: [
+ SignOutComponent
+ ],
+ schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ })
+ .compileComponents();
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(SignOutComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+ }));
+
+ it("should create an instance", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/log-out/log-out.component.ts b/src/app/shared/log-out/log-out.component.ts
new file mode 100644
index 0000000000..36c5914347
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.ts
@@ -0,0 +1,89 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+// @ngrx
+import { Store } from '@ngrx/store';
+
+// actions
+import { LogOutAction } from '../../core/auth/auth.actions';
+
+// reducers
+import {
+ getLogOutError,
+ isAuthenticated,
+ isAuthenticationLoading,
+} from '../../core/auth/selectors';
+
+import { AppState } from '../../app.reducer';
+import { Observable } from 'rxjs/Observable';
+import { fadeOut } from '../animations/fade';
+
+@Component({
+ selector: 'ds-log-out',
+ templateUrl: './log-out.component.html',
+ styleUrls: ['./log-out.component.scss'],
+ animations: [fadeOut]
+})
+export class LogOutComponent implements OnDestroy, OnInit {
+ /**
+ * The error if authentication fails.
+ * @type {Observable}
+ */
+ public error: Observable;
+
+ /**
+ * True if the logout is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ /**
+ * Component state.
+ * @type {boolean}
+ */
+ private alive = true;
+
+ /**
+ * @constructor
+ * @param {Store} store
+ */
+ constructor(private router: Router,
+ private store: Store) { }
+
+ /**
+ * Lifecycle hook that is called when a directive, pipe or service is destroyed.
+ */
+ public ngOnDestroy() {
+ this.alive = false;
+ }
+
+ /**
+ * Lifecycle hook that is called after data-bound properties of a directive are initialized.
+ */
+ ngOnInit() {
+ // set error
+ this.error = this.store.select(getLogOutError);
+
+ // set loading
+ this.loading = this.store.select(isAuthenticationLoading);
+ }
+
+ /**
+ * Go to the home page.
+ */
+ public home() {
+ this.router.navigate(['/home']);
+ }
+
+ /**
+ * To to the log in page.
+ */
+ public logIn() {
+ this.router.navigate(['/login']);
+ }
+
+ public logOut() {
+ this.store.dispatch(new LogOutAction());
+ }
+
+}
diff --git a/src/app/shared/login-form/login-form.component.html b/src/app/shared/login-form/login-form.component.html
deleted file mode 100644
index 6dcc4d1a5c..0000000000
--- a/src/app/shared/login-form/login-form.component.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
diff --git a/src/app/shared/login-form/login-form.component.ts b/src/app/shared/login-form/login-form.component.ts
deleted file mode 100644
index 39fb3a8936..0000000000
--- a/src/app/shared/login-form/login-form.component.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'ds-login-form',
- styleUrls: ['./login-form.component.scss'],
- templateUrl: './login-form.component.html'
-})
-export class LoginFormComponent {
-
-}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 2f5a266d9f..0eb2696614 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -41,7 +41,9 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
import { VarDirective } from './utils/var.directive';
-import { LoginFormComponent } from './login-form/login-form.component';
+import { LogInComponent } from './log-in/log-in.component';
+import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
+import { LogOutComponent } from './log-out/log-out.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -64,12 +66,14 @@ const PIPES = [
const COMPONENTS = [
// put shared components here
+ AuthNavMenuComponent,
ComcolPageContentComponent,
ComcolPageHeaderComponent,
ComcolPageLogoComponent,
ErrorComponent,
LoadingComponent,
- LoginFormComponent,
+ LogInComponent,
+ LogOutComponent,
ObjectListComponent,
AbstractListableElementComponent,
WrapperListElementComponent,