Added first release of the authentication module

This commit is contained in:
Giuseppe Digilio
2018-02-06 15:48:05 +01:00
parent 8f28429fe6
commit 19c9482009
49 changed files with 1532 additions and 110 deletions

View File

@@ -47,7 +47,8 @@
}, },
"nav": { "nav": {
"home": "Home", "home": "Home",
"login": "Log In" "login": "Log In",
"logout": "Log Out"
}, },
"pagination": { "pagination": {
"results-per-page": "Results Per Page", "results-per-page": "Results Per Page",
@@ -150,10 +151,19 @@
"login": { "login": {
"title": "Login", "title": "Login",
"form": { "form": {
"header": "Please log in", "header": "Please log in to DSpace",
"email": "Email address", "email": "Email address",
"forgot-password": "Have you forgotten your password?",
"new-user": "New user? Click here to register.",
"password": "Password", "password": "Password",
"submit": "Log in" "submit": "Log in"
} }
},
"logout": {
"title": "Logout",
"form": {
"header": "Log out from DSpace",
"submit": "Log out"
}
} }
} }

View File

@@ -1,5 +1,6 @@
<ds-home-news></ds-home-news> <ds-home-news></ds-home-news>
<div class="container"> <div class="container">
<p *ngIf="(isAuthenticated | async)">Loggato</p>
<ds-search-form></ds-search-form> <ds-search-form></ds-search-form>
<ds-top-level-community-list></ds-top-level-community-list> <ds-top-level-community-list></ds-top-level-community-list>
</div> </div>

View File

@@ -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({ @Component({
selector: 'ds-home-page', selector: 'ds-home-page',
styleUrls: ['./home-page.component.scss'], styleUrls: ['./home-page.component.scss'],
templateUrl: './home-page.component.html' templateUrl: './home-page.component.html'
}) })
export class HomePageComponent { export class HomePageComponent implements OnInit {
public isAuthenticated: Observable<boolean>;
constructor(private store: Store<AppState>) {}
ngOnInit() {
// set loading
this.isAuthenticated = this.store.select(isAuthenticated);
}
} }

View File

@@ -1,5 +1,5 @@
<div class="text-center mt-5"> <div class="text-center mt-5">
<img class="mb-4" src="assets/images/dspace-logo.png" alt="" width="72" height="72"> <img class="mb-4" src="assets/images/dspace-logo.png" alt="" width="72" height="72">
<h1 class="h3 mb-0 font-weight-normal">{{"login.form.header" | translate}}</h1> <h1 class="h3 mb-0 font-weight-normal">{{"login.form.header" | translate}}</h1>
<ds-login-form></ds-login-form> <ds-log-in></ds-log-in>
</div> </div>

View File

@@ -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 { }

View File

@@ -0,0 +1,5 @@
<div class="text-center mt-5">
<img class="mb-4" src="assets/images/dspace-logo.png" alt="" width="72" height="72">
<h1 class="h3 mb-0 font-weight-normal">{{"logout.form.header" | translate}}</h1>
<ds-log-out></ds-log-out>
</div>

View File

@@ -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;
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -13,6 +13,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent },
]) ])
], ],

View File

@@ -1,6 +1,7 @@
import { HeaderEffects } from './header/header.effects'; import { HeaderEffects } from './header/header.effects';
import { StoreEffects } from './store.effects'; import { StoreEffects } from './store.effects';
import { AuthEffects } from './core/auth/auth.effects';
export const appEffects = [ export const appEffects = [
StoreEffects, StoreEffects,

View File

@@ -11,6 +11,7 @@ import {
filterReducer, filterReducer,
SearchFiltersState SearchFiltersState
} from './+search-page/search-filters/search-filter/search-filter.reducer'; } from './+search-page/search-filters/search-filter/search-filter.reducer';
import { authReducer, AuthState } from './core/auth/auth.reducers';
export interface AppState { export interface AppState {
router: fromRouter.RouterReducerState; router: fromRouter.RouterReducerState;

View File

@@ -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<DSpaceObject> {
switch (type) {
case AuthType.Status: {
return AuthStatus
}
default: {
return undefined;
}
}
}
}

View File

@@ -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<any> {
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<any> {
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();
}
}

View File

@@ -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<ConfigObject,ConfigType>(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)
}
}

View File

@@ -0,0 +1,3 @@
export enum AuthType {
Status = 'status'
}

View File

@@ -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;

View File

@@ -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<Action> = 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<Action> = 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<Action> = 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<Action> = 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) {
}
}

View File

@@ -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;

View File

@@ -3,6 +3,9 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Eperson } from '../eperson/models/eperson.model'; 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(); export const MOCK_USER = new Eperson();
MOCK_USER.id = '92a59227-ccf7-46da-9776-86c3fc64147f'; MOCK_USER.id = '92a59227-ccf7-46da-9776-86c3fc64147f';
@@ -27,6 +30,8 @@ MOCK_USER.metadata = [
} }
]; ];
export const TOKENITEM = 'ds-token';
/** /**
* The user service. * The user service.
*/ */
@@ -39,18 +44,36 @@ export class AuthService {
*/ */
private _authenticated = false; private _authenticated = false;
constructor(private authRequestService: AuthRequestService) {}
/** /**
* Authenticate the user * Authenticate the user
* *
* @param {string} email The user's email address * @param {string} user The user name
* @param {string} password The user's password * @param {string} password The user's password
* @returns {Observable<User>} The authenticated user observable. * @returns {Observable<User>} The authenticated user observable.
*/ */
public authenticate(email: string, password: string): Observable<Eperson> { public authenticate(user: string, password: string): Observable<Eperson> {
// Normally you would do an HTTP request to determine to // Normally you would do an HTTP request to determine to
// attempt authenticating the user using the supplied credentials. // attempt authenticating the user using the supplied credentials.
// const body = `user=${user}&password=${password}`;
if (email === MOCK_USER.email && 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; this._authenticated = true;
return Observable.of(MOCK_USER); return Observable.of(MOCK_USER);
} }

View File

@@ -5,10 +5,8 @@ import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
// reducers // reducers
import { import { CoreState } from '../core.reducers';
isAuthenticated, import { isAuthenticated } from './selectors';
State
} from '../app.reducers';
/** /**
* Prevent unauthorized activating and loading of routes * Prevent unauthorized activating and loading of routes
@@ -20,7 +18,7 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
/** /**
* @constructor * @constructor
*/ */
constructor(private router: Router, private store: Store<State>) {} constructor(private router: Router, private store: Store<CoreState>) {}
/** /**
* True when user is authenticated * True when user is authenticated

View File

@@ -1,9 +0,0 @@
import { DSpaceObject } from '../shared/dspace-object.model';
export class AuthenticationStatus extends DSpaceObject {
okay: boolean;
authenticated: boolean;
}

View File

@@ -0,0 +1,5 @@
export interface AuthInfo {
access_token?: string,
expires?: number,
expires_in?: number
}

View File

@@ -0,0 +1,9 @@
import { DSpaceObject } from '../../shared/dspace-object.model';
export class AuthStatus extends DSpaceObject {
okay: boolean;
authenticated: boolean;
}

View File

@@ -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);

View File

@@ -62,4 +62,14 @@ export class ConfigSuccessResponse extends RestResponse {
super(true, statusCode); 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 */ /* tslint:enable:max-classes-per-file */

View File

@@ -3,10 +3,12 @@ import { ObjectCacheEffects } from './cache/object-cache.effects';
import { ResponseCacheEffects } from './cache/response-cache.effects'; import { ResponseCacheEffects } from './cache/response-cache.effects';
import { UUIDIndexEffects } from './index/index.effects'; import { UUIDIndexEffects } from './index/index.effects';
import { RequestEffects } from './data/request.effects'; import { RequestEffects } from './data/request.effects';
import { AuthEffects } from './auth/auth.effects';
export const coreEffects = [ export const coreEffects = [
ResponseCacheEffects, ResponseCacheEffects,
RequestEffects, RequestEffects,
ObjectCacheEffects, ObjectCacheEffects,
UUIDIndexEffects, UUIDIndexEffects,
AuthEffects,
]; ];

View File

@@ -38,6 +38,9 @@ import { SubmissionDefinitionsConfigService } from './config/submission-definiti
import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
import { UUIDService } from './shared/uuid.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 = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -55,6 +58,9 @@ const EXPORTS = [
const PROVIDERS = [ const PROVIDERS = [
ApiService, ApiService,
AuthenticatedGuard,
AuthRequestService,
AuthService,
CommunityDataService, CommunityDataService,
CollectionDataService, CollectionDataService,
DSOResponseParsingService, DSOResponseParsingService,

View File

@@ -4,19 +4,22 @@ import { responseCacheReducer, ResponseCacheState } from './cache/response-cache
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
import { indexReducer, IndexState } from './index/index.reducer'; import { indexReducer, IndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer'; import { requestReducer, RequestState } from './data/request.reducer';
import { authReducer, AuthState } from './auth/auth.reducers';
export interface CoreState { export interface CoreState {
'data/object': ObjectCacheState, 'data/object': ObjectCacheState,
'data/response': ResponseCacheState, 'data/response': ResponseCacheState,
'data/request': RequestState, 'data/request': RequestState,
'index': IndexState 'index': IndexState,
'auth': AuthState,
} }
export const coreReducers: ActionReducerMap<CoreState> = { export const coreReducers: ActionReducerMap<CoreState> = {
'data/object': objectCacheReducer, 'data/object': objectCacheReducer,
'data/response': responseCacheReducer, 'data/response': responseCacheReducer,
'data/request': requestReducer, 'data/request': requestReducer,
'index': indexReducer 'index': indexReducer,
'auth': authReducer
}; };
export const coreSelector = createFeatureSelector<CoreState>('core'); export const coreSelector = createFeatureSelector<CoreState>('core');

View File

@@ -33,9 +33,9 @@ export class RequestEffects {
let body; let body;
if (isNotEmpty(request.body)) { if (isNotEmpty(request.body)) {
const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type)); 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) => .map((data: DSpaceRESTV2Response) =>
this.injector.get(request.getResponseParser()).parse(request, data)) this.injector.get(request.getResponseParser()).parse(request, data))
.do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive)) .do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))

View File

@@ -7,6 +7,9 @@ import { ResponseParsingService } from './parsing.service';
import { RootResponseParsingService } from './root-response-parsing.service'; import { RootResponseParsingService } from './root-response-parsing.service';
import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { BrowseResponseParsingService } from './browse-response-parsing.service';
import { ConfigResponseParsingService } from './config-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 */ /* tslint:disable:max-classes-per-file */
@@ -34,7 +37,8 @@ export abstract class RestRequest {
public uuid: string, public uuid: string,
public href: string, public href: string,
public method: RestRequestMethod = RestRequestMethod.Get, public method: RestRequestMethod = RestRequestMethod.Get,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
} }
@@ -47,7 +51,8 @@ export class GetRequest extends RestRequest {
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.Get, body) super(uuid, href, RestRequestMethod.Get, body)
} }
@@ -57,7 +62,8 @@ export class PostRequest extends RestRequest {
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.Post, body) super(uuid, href, RestRequestMethod.Post, body)
} }
@@ -67,7 +73,8 @@ export class PutRequest extends RestRequest {
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.Put, body) super(uuid, href, RestRequestMethod.Put, body)
} }
@@ -77,7 +84,8 @@ export class DeleteRequest extends RestRequest {
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.Delete, body) super(uuid, href, RestRequestMethod.Delete, body)
} }
@@ -87,7 +95,8 @@ export class OptionsRequest extends RestRequest {
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.Options, body) super(uuid, href, RestRequestMethod.Options, body)
} }
@@ -97,7 +106,8 @@ export class HeadRequest extends RestRequest {
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.Head, body) super(uuid, href, RestRequestMethod.Head, body)
} }
@@ -107,7 +117,8 @@ export class PatchRequest extends RestRequest {
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any public body?: any,
public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.Patch, body) super(uuid, href, RestRequestMethod.Patch, body)
} }
@@ -134,7 +145,7 @@ export class FindAllRequest extends GetRequest {
constructor( constructor(
uuid: string, uuid: string,
href: string, href: string,
public options?: FindAllOptions, public body?: FindAllOptions,
) { ) {
super(uuid, href); 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<ResponseParsingService> {
return AuthResponseParsingService;
}
}
export class AuthGetRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return AuthResponseParsingService;
}
}
export class RequestError extends Error { export class RequestError extends Error {
statusText: string; statusText: string;
} }

View File

@@ -1,10 +1,21 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Request } from '@angular/http'; 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 { Observable } from 'rxjs/Observable';
import { RestRequestMethod } from '../data/request.models'; import { RestRequestMethod } from '../data/request.models';
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; 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 * Service to access DSpace's REST API
@@ -45,8 +56,25 @@ export class DSpaceRESTv2Service {
* @return {Observable<string>} * @return {Observable<string>}
* An Observable<string> containing the response from the server * An Observable<string> containing the response from the server
*/ */
request(method: RestRequestMethod, url: string, body?: any): Observable<DSpaceRESTV2Response> { request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
return this.http.request(method, url, { body, observe: 'response' }) 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 })) .map((res) => ({ payload: res.body, statusCode: res.statusText }))
.catch((err) => { .catch((err) => {
console.log('Error: ', err); console.log('Error: ', err);

View File

@@ -23,9 +23,10 @@ export abstract class HALEndpointService {
.distinctUntilChanged(); .distinctUntilChanged();
} }
public getEndpoint(): Observable<string> { public getEndpoint(linkName?: string): Observable<string> {
const mapLinkName = isNotEmpty(linkName) ? linkName : this.linkName;
return this.getEndpointMap() return this.getEndpointMap()
.map((map: EndpointMap) => map[this.linkName]) .map((map: EndpointMap) => map[mapLinkName])
.distinctUntilChanged(); .distinctUntilChanged();
} }

View File

@@ -12,19 +12,7 @@
<a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a> <a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a>
</li> </li>
</ul> </ul>
<ul class="navbar-nav" [ngClass]="{'mr-auto': (windowService.isMobileView() | async)}"> <ds-auth-nav-menu></ds-auth-nav-menu>
<li *ngIf="!(windowService.isMobileView() | async)" class="nav-item dropdown" (click)="$event.stopPropagation();">
<div ngbDropdown placement="bottom-right" class="d-inline-block float-right" @fadeInOut>
<a href="#" id="dropdownLogin" class="nav-link" (click)="$event.preventDefault()" ngbDropdownToggle><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="caret"></span></a>
<div ngbDropdownMenu aria-labelledby="dropdownLogin">
<ds-login-form></ds-login-form>
</div>
</div>
</li>
<li *ngIf="(windowService.isMobileView() | async)" class="nav-item">
<a class="nav-link" routerLink="/login" routerLinkActive="active"><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
</li>
</ul>
</div> </div>
</nav> </nav>
</header> </header>

View File

@@ -1,12 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { createSelector, Store } from '@ngrx/store'; import { createSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RouterReducerState } from '@ngrx/router-store';
import { HeaderState } from './header.reducer'; import { HeaderState } from './header.reducer';
import { HeaderToggleAction } from './header.actions'; import { HeaderToggleAction } from './header.actions';
import { AppState } from '../app.reducer'; import { AppState } from '../app.reducer';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { fadeInOut } from '../shared/animations/fade';
const headerStateSelector = (state: AppState) => state.header; const headerStateSelector = (state: AppState) => state.header;
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed); const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
@@ -15,12 +15,15 @@ const navCollapsedSelector = createSelector(headerStateSelector, (header: Header
selector: 'ds-header', selector: 'ds-header',
styleUrls: ['header.component.scss'], styleUrls: ['header.component.scss'],
templateUrl: 'header.component.html', templateUrl: 'header.component.html',
animations: [
fadeInOut
]
}) })
export class HeaderComponent implements OnInit { export class HeaderComponent implements OnInit {
/**
* Whether user is authenticated.
* @type {Observable<string>}
*/
public isAuthenticated: Observable<boolean>;
public isNavBarCollapsed: Observable<boolean>; public isNavBarCollapsed: Observable<boolean>;
public showAuth = false;
constructor( constructor(
private store: Store<AppState>, private store: Store<AppState>,
@@ -29,6 +32,7 @@ export class HeaderComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
// set loading
this.isNavBarCollapsed = this.store.select(navCollapsedSelector); this.isNavBarCollapsed = this.store.select(navCollapsedSelector);
} }

View File

@@ -0,0 +1,26 @@
<ul class="navbar-nav" [ngClass]="{'mr-auto': (windowService.isMobileView() | async)}">
<li *ngIf="!(isAuthenticated | async) && !(windowService.isMobileView() | async) && showAuth" class="nav-item dropdown" (click)="$event.stopPropagation();">
<div ngbDropdown placement="bottom-right" class="d-inline-block float-right" @fadeInOut>
<a href="#" id="dropdownLogin" class="nav-link" (click)="$event.preventDefault()" ngbDropdownToggle><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="caret"></span></a>
<div ngbDropdownMenu aria-labelledby="dropdownLogin">
<ds-log-in></ds-log-in>
</div>
</div>
</li>
<li *ngIf="!(isAuthenticated | async) && (windowService.isMobileView() | async)" class="nav-item">
<a class="nav-link" routerLink="/login" routerLinkActive="active"><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
</li>
<li *ngIf="(isAuthenticated | async) && !(windowService.isMobileView() | async)" class="nav-item">
<div ngbDropdown placement="bottom-right" class="d-inline-block" [ngClass]="{'float-right': !(windowService.isMobileView() | async)}" @fadeInOut>
<a href="#" id="dropdownUser" class="nav-link" (click)="$event.preventDefault()" ngbDropdownToggle><i class="fa fa-user fa-fw" aria-hidden="true"></i>Hello {{(user | async).name}}<span class="caret"></span></a>
<div ngbDropdownMenu aria-labelledby="dropdownUser">
<ds-log-out></ds-log-out>
</div>
</div>
</li>
<li *ngIf="(isAuthenticated | async) && (windowService.isMobileView() | async)" class="nav-item">
<a class="nav-link" routerLink="/logout" routerLinkActive="active"><i class="fa fa-sign-out fa-fw" aria-hidden="true"></i> {{ 'nav.logout' | translate }}<span class="sr-only">(current)</span></a>
</li>
</ul>

View File

@@ -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<string>}
*/
public isAuthenticated: Observable<boolean>;
public showAuth = false;
public user: Observable<Eperson>;
protected subs: Subscription[] = [];
constructor(
private appStore: Store<AppState>,
private coreStore: Store<CoreState>,
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());
}
}

View File

@@ -0,0 +1,29 @@
<ds-loading *ngIf="(loading | async)"></ds-loading>
<form *ngIf="!(loading | async)" class="form-login px-4 py-3" (ngSubmit)="submit()" [formGroup]="form" novalidate>
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
<input id="inputEmail"
autocomplete="off"
autofocus
class="form-control"
formControlName="email"
placeholder="{{'login.form.email' | translate}}"
required
type="email"
(input)="resetError($event)">
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
<input id="inputPassword"
autocomplete="off"
class="form-control"
placeholder="{{'login.form.password' | translate}}"
formControlName="password"
required
type="password"
(input)="resetError($event)">
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</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>
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
</form>

View File

@@ -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<LogInComponent>;
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<LogInComponent>) {
// 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;
}
}

View File

@@ -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<string>}
*/
public error: Observable<string>;
/**
* Has authentication error.
* @type {boolean}
*/
public hasError = false;
/**
* True if the authentication is loading.
* @type {boolean}
*/
public loading: Observable<boolean>;
/**
* The authentication form.
* @type {FormGroup}
*/
public form: FormGroup;
/**
* Component state.
* @type {boolean}
*/
private alive = true;
/**
* @constructor
* @param {FormBuilder} formBuilder
* @param {Store<State>} store
*/
constructor(
private formBuilder: FormBuilder,
private router: Router,
private store: Store<CoreState>
) { }
/**
* 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();
}
}

View File

@@ -0,0 +1,9 @@
<ds-loading *ngIf="(loading | async)"></ds-loading>
<div *ngIf="!(loading | async)" class="form-login px-4 py-3">
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div>
<button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()">{{"logout.form.submit" | translate}}</button>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
</div>

View File

@@ -0,0 +1 @@
@import '../log-in/log-in.component.scss';

View File

@@ -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<SignOutComponent>;
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();
});
});

View File

@@ -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<string>}
*/
public error: Observable<string>;
/**
* True if the logout is loading.
* @type {boolean}
*/
public loading: Observable<boolean>;
/**
* Component state.
* @type {boolean}
*/
private alive = true;
/**
* @constructor
* @param {Store<State>} store
*/
constructor(private router: Router,
private store: Store<AppState>) { }
/**
* 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());
}
}

View File

@@ -1,11 +0,0 @@
<form class="form-login px-4 py-3">
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
<input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit">{{"login.form.submit" | translate}}</button>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">New around here? Sign up</a>
<a class="dropdown-item" href="#">Forgot password?</a>
</form>

View File

@@ -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 {
}

View File

@@ -41,7 +41,9 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component'; import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component'; import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
import { VarDirective } from './utils/var.directive'; 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 = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -64,12 +66,14 @@ const PIPES = [
const COMPONENTS = [ const COMPONENTS = [
// put shared components here // put shared components here
AuthNavMenuComponent,
ComcolPageContentComponent, ComcolPageContentComponent,
ComcolPageHeaderComponent, ComcolPageHeaderComponent,
ComcolPageLogoComponent, ComcolPageLogoComponent,
ErrorComponent, ErrorComponent,
LoadingComponent, LoadingComponent,
LoginFormComponent, LogInComponent,
LogOutComponent,
ObjectListComponent, ObjectListComponent,
AbstractListableElementComponent, AbstractListableElementComponent,
WrapperListElementComponent, WrapperListElementComponent,