Auth module improvement

This commit is contained in:
Giuseppe Digilio
2018-02-12 19:13:42 +01:00
parent b7cff01dab
commit 8df514f39c
15 changed files with 238 additions and 89 deletions

View File

@@ -28,3 +28,5 @@ export const appReducers: ActionReducerMap<AppState> = {
searchSidebar: sidebarReducer, searchSidebar: sidebarReducer,
searchFilter: filterReducer searchFilter: filterReducer
}; };
export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -17,6 +17,7 @@ export const AuthActionTypes = {
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'), AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'), CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'), CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
REDIRECT: type('dspace/auth/REDIRECT'),
RESET_ERROR: type('dspace/auth/RESET_ERROR'), RESET_ERROR: type('dspace/auth/RESET_ERROR'),
LOG_OUT: type('dspace/auth/LOG_OUT'), LOG_OUT: type('dspace/auth/LOG_OUT'),
LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'), LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'),
@@ -136,15 +137,6 @@ export class CheckAuthenticationTokenErrorAction implements Action {
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR; public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR;
} }
/**
* Reset error.
* @class ResetAuthenticationErrorAction
* @implements {Action}
*/
export class ResetAuthenticationErrorAction implements Action {
public type: string = AuthActionTypes.RESET_ERROR;
}
/** /**
* Sign out. * Sign out.
* @class LogOutAction * @class LogOutAction
@@ -179,6 +171,20 @@ export class LogOutSuccessAction implements Action {
constructor(public payload?: any) {} constructor(public payload?: any) {}
} }
/**
* Redirect to login page when token is expired.
* @class RedirectWhenTokenExpiredAction
* @implements {Action}
*/
export class RedirectWhenTokenExpiredAction implements Action {
public type: string = AuthActionTypes.REDIRECT;
payload: string;
constructor(message: string) {
this.payload = message ;
}
}
/** /**
* Sign up. * Sign up.
* @class RegistrationAction * @class RegistrationAction
@@ -221,6 +227,15 @@ export class RegistrationSuccessAction implements Action {
} }
} }
/**
* Reset error.
* @class ResetAuthenticationErrorAction
* @implements {Action}
*/
export class ResetAuthenticationErrorAction implements Action {
public type: string = AuthActionTypes.RESET_ERROR;
}
/* tslint:enable:max-classes-per-file */ /* tslint:enable:max-classes-per-file */
/** /**
@@ -237,6 +252,7 @@ export type AuthActions
| AuthenticationSuccessAction | AuthenticationSuccessAction
| CheckAuthenticationTokenAction | CheckAuthenticationTokenAction
| CheckAuthenticationTokenErrorAction | CheckAuthenticationTokenErrorAction
| RedirectWhenTokenExpiredAction
| RegistrationAction | RegistrationAction
| RegistrationErrorAction | RegistrationErrorAction
| RegistrationSuccessAction; | RegistrationSuccessAction;

View File

@@ -16,7 +16,7 @@ import {
AuthenticatedErrorAction, AuthenticatedErrorAction,
AuthenticatedSuccessAction, AuthenticatedSuccessAction,
AuthenticationErrorAction, AuthenticationErrorAction,
AuthenticationSuccessAction, CheckAuthenticationTokenAction, CheckAuthenticationTokenErrorAction, LogOutAction, AuthenticationSuccessAction, CheckAuthenticationTokenErrorAction,
LogOutErrorAction, LogOutErrorAction,
LogOutSuccessAction, RegistrationAction, LogOutSuccessAction, RegistrationAction,
RegistrationErrorAction, RegistrationErrorAction,
@@ -87,7 +87,7 @@ export class AuthEffects {
/** /**
* When the store is rehydrated in the browser, * When the store is rehydrated in the browser,
* clear a possible invalid token * clear a possible invalid token or authentication errors
*/ */
@Effect({dispatch: false}) @Effect({dispatch: false})
public clearInvalidTokenOnRehydrate = this.actions$ public clearInvalidTokenOnRehydrate = this.actions$
@@ -96,7 +96,8 @@ export class AuthEffects {
return this.store.select(isAuthenticated) return this.store.select(isAuthenticated)
.take(1) .take(1)
.filter((authenticated) => !authenticated) .filter((authenticated) => !authenticated)
.do(() => this.authService.removeToken()); .do(() => this.authService.removeToken())
.do(() => this.authService.resetAuthenticationError());
}); });
@Effect() @Effect()
@@ -113,10 +114,16 @@ export class AuthEffects {
.ofType(AuthActionTypes.LOG_OUT_SUCCESS) .ofType(AuthActionTypes.LOG_OUT_SUCCESS)
.do((action: LogOutSuccessAction) => this.authService.removeToken()); .do((action: LogOutSuccessAction) => this.authService.removeToken());
@Effect({dispatch: false})
public redirectToLogin: Observable<Action> = this.actions$
.ofType(AuthActionTypes.REDIRECT)
.do(() => this.authService.redirectToLogin());
/** /**
* @constructor * @constructor
* @param {Actions} actions$ * @param {Actions} actions$
* @param {AuthService} authService * @param {AuthService} authService
* @param {Store} store
*/ */
constructor(private actions$: Actions, constructor(private actions$: Actions,
private authService: AuthService, private authService: AuthService,

View File

@@ -14,10 +14,13 @@ import { AuthType } from './auth-type';
import { ResourceType } from '../shared/resource-type'; import { ResourceType } from '../shared/resource-type';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { AppState } from '../../app.reducer';
import { RedirectWhenTokenExpiredAction } from './auth.actions';
import { Store } from '@ngrx/store';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
constructor(private inj: Injector, private router: Router) { } constructor(private inj: Injector, private store: Store<AppState>) { }
private isUnauthorized(status: number): boolean { private isUnauthorized(status: number): boolean {
return status === 401 || status === 403; return status === 401 || status === 403;
@@ -84,15 +87,21 @@ export class AuthInterceptor implements HttpInterceptor {
.catch((error, caught) => { .catch((error, caught) => {
// Intercept an unauthorized error response // Intercept an unauthorized error response
if (error instanceof HttpErrorResponse && this.isUnauthorized(error.status)) { if (error instanceof HttpErrorResponse && this.isUnauthorized(error.status)) {
// Create a new HttpResponse and return it, so it can be handle properly by AuthService. // Checks if is a response from a request to an authentication endpoint
const authResponse = new HttpResponse({ if (this.isAuthRequest(error.url)) {
body: this.makeAuthStatusObject(false, null, error.error), // Create a new HttpResponse and return it, so it can be handle properly by AuthService.
headers: error.headers, const authResponse = new HttpResponse({
status: error.status, body: this.makeAuthStatusObject(false, null, error.error),
statusText: error.statusText, headers: error.headers,
url: error.url status: error.status,
}); statusText: error.statusText,
return Observable.of(authResponse); url: error.url
});
return Observable.of(authResponse);
} else {
// Redirect to the login route
this.store.dispatch(new RedirectWhenTokenExpiredAction('Your session has expired. Please log in again.'));
}
} else { } else {
// Return error response as is. // Return error response as is.
return Observable.throw(error); return Observable.throw(error);

View File

@@ -1,7 +1,7 @@
// import actions // import actions
import { import {
AuthActions, AuthActionTypes, AuthenticatedSuccessAction, AuthenticationErrorAction, AuthActions, AuthActionTypes, AuthenticatedSuccessAction, AuthenticationErrorAction,
AuthenticationSuccessAction, LogOutErrorAction AuthenticationSuccessAction, LogOutErrorAction, RedirectWhenTokenExpiredAction
} from './auth.actions'; } from './auth.actions';
// import models // import models
@@ -25,6 +25,9 @@ export interface AuthState {
// true when loading // true when loading
loading: boolean; loading: boolean;
// info message
message?: string;
// the authenticated user // the authenticated user
user?: Eperson; user?: Eperson;
} }
@@ -33,7 +36,7 @@ export interface AuthState {
* The initial state. * The initial state.
*/ */
const initialState: AuthState = { const initialState: AuthState = {
authenticated: null, authenticated: false,
loaded: false, loaded: false,
loading: false loading: false
}; };
@@ -50,7 +53,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
case AuthActionTypes.AUTHENTICATE: case AuthActionTypes.AUTHENTICATE:
return Object.assign({}, state, { return Object.assign({}, state, {
error: undefined, error: undefined,
loading: true loading: true,
message: undefined
}); });
case AuthActionTypes.AUTHENTICATED_ERROR: case AuthActionTypes.AUTHENTICATED_ERROR:
@@ -67,6 +71,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
loaded: true, loaded: true,
error: undefined, error: undefined,
loading: false, loading: false,
message: undefined,
user: (action as AuthenticatedSuccessAction).payload.user user: (action as AuthenticatedSuccessAction).payload.user
}); });
@@ -96,10 +101,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
case AuthActionTypes.RESET_ERROR: case AuthActionTypes.RESET_ERROR:
return Object.assign({}, state, { return Object.assign({}, state, {
authenticated: null, authenticated: false,
error: undefined, error: undefined,
loaded: false, loaded: false,
loading: false, loading: false,
message: undefined,
}); });
case AuthActionTypes.LOG_OUT_ERROR: case AuthActionTypes.LOG_OUT_ERROR:
@@ -115,6 +121,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
error: undefined, error: undefined,
loaded: false, loaded: false,
loading: false, loading: false,
message: undefined,
user: undefined
});
case AuthActionTypes.REDIRECT:
return Object.assign({}, state, {
authenticated: false,
loaded: false,
loading: false,
message: (action as RedirectWhenTokenExpiredAction).payload,
user: undefined user: undefined
}); });
@@ -122,7 +138,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
return Object.assign({}, state, { return Object.assign({}, state, {
authenticated: false, authenticated: false,
error: undefined, error: undefined,
loading: true loading: true,
message: undefined
}); });
default: default:
@@ -158,10 +175,18 @@ export const getAuthenticatedUser = (state: AuthState) => state.user;
* Returns the authentication error. * Returns the authentication error.
* @function getAuthenticationError * @function getAuthenticationError
* @param {State} state * @param {State} state
* @returns {Error} * @returns {String}
*/ */
export const getAuthenticationError = (state: AuthState) => state.error; export const getAuthenticationError = (state: AuthState) => state.error;
/**
* Returns the authentication info message.
* @function getAuthenticationError
* @param {State} state
* @returns {String}
*/
export const getAuthenticationMessage = (state: AuthState) => state.message;
/** /**
* Returns true if request is in progress. * Returns true if request is in progress.
* @function isLoading * @function isLoading

View File

@@ -8,9 +8,16 @@ import { HttpHeaders } from '@angular/common/http';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
import { isNotEmpty, isNotNull } from '../../shared/empty.util'; import { isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
import { CookieService } from '../../shared/services/cookie.service'; import { CookieService } from '../../shared/services/cookie.service';
import { ActivatedRoute, Router } from '@angular/router';
import { isAuthenticated } from './selectors';
import { AppState, routerStateSelector } from '../../app.reducer';
import { Store } from '@ngrx/store';
import { ResetAuthenticationErrorAction } from './auth.actions';
import { RouterReducerState } from '@ngrx/router-store';
export const LOGIN_ROUTE = '/login';
/** /**
* The auth service. * The auth service.
*/ */
@@ -21,7 +28,7 @@ export class AuthService {
* True if authenticated * True if authenticated
* @type boolean * @type boolean
*/ */
private _authenticated = false; private _authenticated: boolean;
/** /**
* The url to redirect after login * The url to redirect after login
@@ -29,7 +36,27 @@ export class AuthService {
*/ */
private _redirectUrl: string; private _redirectUrl: string;
constructor(private authRequestService: AuthRequestService, private storage: CookieService) { constructor(private route: ActivatedRoute,
private authRequestService: AuthRequestService,
private router: Router,
private storage: CookieService,
private store: Store<AppState>) {
this.store.select(isAuthenticated)
.startWith(false)
.subscribe((authenticated: boolean) => this._authenticated = authenticated);
// If current route is different from the one setted in authentication guard
// and is not the login route, clear it
this.store.select(routerStateSelector)
.filter((routerState: RouterReducerState) => isNotUndefined(routerState))
.filter((routerState: RouterReducerState) =>
(routerState.state.url !== LOGIN_ROUTE)
&& isNotEmpty(this._redirectUrl)
&& (routerState.state.url !== this._redirectUrl))
.distinctUntilChanged()
.subscribe((routerState: RouterReducerState) => {
this._redirectUrl = '';
})
} }
/** /**
@@ -40,18 +67,12 @@ export class AuthService {
* @returns {Observable<User>} The authenticated user observable. * @returns {Observable<User>} The authenticated user observable.
*/ */
public authenticate(user: string, password: string): Observable<AuthStatus> { public authenticate(user: string, password: string): Observable<AuthStatus> {
// 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}`;
// const body = encodeURI('password=test&user=vera.aloe@mailinator.com');
// const body = [{user}, {password}];
// const body = encodeURI('password=' + password.toString() + '&user=' + user.toString());
const body = encodeURI(`password=${password}&user=${user}`); const body = encodeURI(`password=${password}&user=${user}`);
const options: HttpOptions = Object.create({}); const options: HttpOptions = Object.create({});
let headers = new HttpHeaders(); let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
options.headers = headers; options.headers = headers;
// options.responseType = 'text';
return this.authRequestService.postToEndpoint('login', body, options) return this.authRequestService.postToEndpoint('login', body, options)
.map((status: AuthStatus) => { .map((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
@@ -67,8 +88,8 @@ export class AuthService {
* Determines if the user is authenticated * Determines if the user is authenticated
* @returns {Observable<boolean>} * @returns {Observable<boolean>}
*/ */
public authenticated(): Observable<boolean> { public isAuthenticated(): Observable<boolean> {
return Observable.of(this._authenticated); return this.store.select(isAuthenticated);
} }
/** /**
@@ -85,10 +106,8 @@ export class AuthService {
return this.authRequestService.getRequest('status', options) return this.authRequestService.getRequest('status', options)
.map((status: AuthStatus) => { .map((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
this._authenticated = true;
return status.eperson[0]; return status.eperson[0];
} else { } else {
this._authenticated = false;
throw(new Error('Not authenticated')); throw(new Error('Not authenticated'));
} }
}); });
@@ -102,6 +121,13 @@ export class AuthService {
return isNotEmpty(token) ? Observable.of(token) : Observable.throw(false); return isNotEmpty(token) ? Observable.of(token) : Observable.throw(false);
} }
/**
* Clear authentication errors
*/
public resetAuthenticationError(): void {
this.store.dispatch(new ResetAuthenticationErrorAction());
}
/** /**
* Create a new user * Create a new user
* @returns {User} * @returns {User}
@@ -119,15 +145,13 @@ export class AuthService {
* @returns {Observable<boolean>} * @returns {Observable<boolean>}
*/ */
public logout(): Observable<boolean> { public logout(): Observable<boolean> {
// Normally you would do an HTTP request sign end the session // Send a request that sign end the session
// but, let's just return an observable of true.
let headers = new HttpHeaders(); let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
const options: HttpOptions = Object.create({headers, responseType: 'text'}); const options: HttpOptions = Object.create({headers, responseType: 'text'});
return this.authRequestService.getRequest('logout', options) return this.authRequestService.getRequest('logout', options)
.map((status: AuthStatus) => { .map((status: AuthStatus) => {
if (!status.authenticated) { if (!status.authenticated) {
this._authenticated = false;
return true; return true;
} else { } else {
throw(new Error('Invalid email or password')); throw(new Error('Invalid email or password'));
@@ -136,32 +160,77 @@ export class AuthService {
} }
/**
* Retrieve authentication token info and make authorization header
* @returns {string}
*/
public getAuthHeader(): string { public getAuthHeader(): string {
// Retrieve authentication token info const token = this.getToken();
const token = this.storage.get(TOKENITEM); return (this._authenticated && isNotNull(token)) ? `Bearer ${token.accessToken}` : '';
return (isNotNull(token) && this._authenticated) ? `Bearer ${token.accessToken}` : '';
} }
/**
* Get authentication token info
* @returns {AuthTokenInfo}
*/
public getToken(): AuthTokenInfo { public getToken(): AuthTokenInfo {
// Retrieve authentication token info // Retrieve authentication token info and check if is valid
return this.storage.get(TOKENITEM); const token = this.storage.get(TOKENITEM);
if (isNotEmpty(token) && token.hasOwnProperty('accessToken') && isNotEmpty(token.accessToken)) {
return token;
} else {
return null;
}
} }
/**
* Save authentication token info
*
* @param {AuthTokenInfo} token The token to save
* @returns {AuthTokenInfo}
*/
public storeToken(token: AuthTokenInfo) { public storeToken(token: AuthTokenInfo) {
// Save authentication token info
return this.storage.set(TOKENITEM, token); return this.storage.set(TOKENITEM, token);
} }
/**
* Remove authentication token info
*/
public removeToken() { public removeToken() {
// Remove authentication token info
console.log('REMOVE!!!!');
return this.storage.remove(TOKENITEM); return this.storage.remove(TOKENITEM);
} }
/**
* Redirect to the login route
*/
public redirectToLogin() {
this.router.navigate(['/login']);
}
/**
* Redirect to the route navigated before the login
*/
public redirectToPreviousUrl() {
if (isNotEmpty(this._redirectUrl)) {
const url = this._redirectUrl;
// Clear url
this._redirectUrl = null;
this.router.navigate([url]);
} else {
this.router.navigate(['/']);
}
}
/**
* Get redirect url
*/
get redirectUrl(): string { get redirectUrl(): string {
return this._redirectUrl; return this._redirectUrl;
} }
/**
* Set redirect url
*/
set redirectUrl(value: string) { set redirectUrl(value: string) {
this._redirectUrl = value; this._redirectUrl = value;
} }

View File

@@ -6,7 +6,7 @@ import { Store } from '@ngrx/store';
// reducers // reducers
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { isAuthenticated } from './selectors'; import { isAuthenticated, isAuthenticationLoading } from './selectors';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
/** /**
@@ -50,6 +50,7 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
} }
private handleAuth(url: string): Observable<boolean> { private handleAuth(url: string): Observable<boolean> {
console.log('handleAuth', url)
// get observable // get observable
const observable = this.store.select(isAuthenticated); const observable = this.store.select(isAuthenticated);
@@ -57,7 +58,7 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
observable.subscribe((authenticated) => { observable.subscribe((authenticated) => {
if (!authenticated) { if (!authenticated) {
this.authService.redirectUrl = url; this.authService.redirectUrl = url;
this.router.navigate(['/login']); this.authService.redirectToLogin();
} }
}); });

View File

@@ -38,6 +38,15 @@ export const getAuthenticatedUser = createSelector(getAuthState, auth.getAuthent
*/ */
export const getAuthenticationError = createSelector(getAuthState, auth.getAuthenticationError); export const getAuthenticationError = createSelector(getAuthState, auth.getAuthenticationError);
/**
* Returns the authentication info message.
* @function getAuthenticationError
* @param {AuthState} state
* @param {any} props
* @return {Error}
*/
export const getAuthenticationMessage = createSelector(getAuthState, auth.getAuthenticationMessage);
/** /**
* Returns true if the user is authenticated * Returns true if the user is authenticated
* @function isAuthenticated * @function isAuthenticated

View File

@@ -2,7 +2,7 @@
<li *ngIf="!(isAuthenticated | async) && !(windowService.isMobileView() | async) && showAuth" class="nav-item dropdown" (click)="$event.stopPropagation();"> <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> <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> <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"> <div id="loginDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownLogin">
<ds-log-in></ds-log-in> <ds-log-in></ds-log-in>
</div> </div>
</div> </div>
@@ -13,7 +13,7 @@
<li *ngIf="(isAuthenticated | async) && !(windowService.isMobileView() | async)" class="nav-item"> <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> <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> <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"> <div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
<ds-log-out></ds-log-out> <ds-log-out></ds-log-out>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,7 @@
#loginDropdownMenu, #logoutDropdownMenu {
min-width: 330px;
}
#loginDropdownMenu {
min-height: 260px;
}

View File

@@ -7,14 +7,12 @@ import { Store } from '@ngrx/store';
import { fadeInOut, fadeOut } from '../animations/fade'; import { fadeInOut, fadeOut } from '../animations/fade';
import { CoreState } from '../../core/core.reducers'; import { CoreState } from '../../core/core.reducers';
import { HostWindowService } from '../host-window.service'; import { HostWindowService } from '../host-window.service';
import { AppState } from '../../app.reducer'; import { AppState, routerStateSelector } from '../../app.reducer';
import { hasValue, isNotUndefined } from '../empty.util'; import { hasValue, isNotUndefined } from '../empty.util';
import { getAuthenticatedUser, isAuthenticated } from '../../core/auth/selectors'; import { getAuthenticatedUser, isAuthenticated } from '../../core/auth/selectors';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { Eperson } from '../../core/eperson/models/eperson.model'; import { Eperson } from '../../core/eperson/models/eperson.model';
const routerStateSelector = (state: AppState) => state.router;
@Component({ @Component({
selector: 'ds-auth-nav-menu', selector: 'ds-auth-nav-menu',
templateUrl: './auth-nav-menu.component.html', templateUrl: './auth-nav-menu.component.html',

View File

@@ -9,7 +9,7 @@
placeholder="{{'login.form.email' | translate}}" placeholder="{{'login.form.email' | translate}}"
required required
type="email" type="email"
(input)="resetError($event)"> (input)="resetErrorOrMessage($event)">
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label> <label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
<input id="inputPassword" <input id="inputPassword"
autocomplete="off" autocomplete="off"
@@ -18,8 +18,9 @@
formControlName="password" formControlName="password"
required required
type="password" type="password"
(input)="resetError($event)"> (input)="resetErrorOrMessage($event)">
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div> <div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div>
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert" @fadeOut>{{ message | async }}</div>
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [disabled]="!form.valid">{{"login.form.submit" | translate}}</button> <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> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a> <a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>

View File

@@ -14,15 +14,15 @@ import { AuthenticateAction, ResetAuthenticationErrorAction } from '../../core/a
// reducers // reducers
import { import {
getAuthenticationError, getAuthenticationError, getAuthenticationMessage,
isAuthenticated, isAuthenticated,
isAuthenticationLoading, isAuthenticationLoading,
} from '../../core/auth/selectors'; } from '../../core/auth/selectors';
import { Router } from '@angular/router';
import { CoreState } from '../../core/core.reducers'; import { CoreState } from '../../core/core.reducers';
import { isNotEmpty, isNotNull } from '../empty.util'; import { isNotEmpty } from '../empty.util';
import { fadeOut } from '../animations/fade'; import { fadeOut } from '../animations/fade';
import { AuthService } from '../../core/auth/auth.service';
/** /**
* /users/sign-in * /users/sign-in
@@ -48,6 +48,18 @@ export class LogInComponent implements OnDestroy, OnInit {
*/ */
public hasError = false; public hasError = false;
/**
* The authentication info message.
* @type {Observable<string>}
*/
public message: Observable<string>;
/**
* Has authentication message.
* @type {boolean}
*/
public hasMessage = false;
/** /**
* True if the authentication is loading. * True if the authentication is loading.
* @type {boolean} * @type {boolean}
@@ -68,12 +80,13 @@ export class LogInComponent implements OnDestroy, OnInit {
/** /**
* @constructor * @constructor
* @param {AuthService} authService
* @param {FormBuilder} formBuilder * @param {FormBuilder} formBuilder
* @param {Store<State>} store * @param {Store<State>} store
*/ */
constructor( constructor(
private authService: AuthService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private router: Router,
private store: Store<CoreState> private store: Store<CoreState>
) { } ) { }
@@ -95,6 +108,13 @@ export class LogInComponent implements OnDestroy, OnInit {
return error; return error;
}); });
// set error
this.message = this.store.select(getAuthenticationMessage)
.map((message) => {
this.hasMessage = (isNotEmpty(message));
return message;
});
// set loading // set loading
this.loading = this.store.select(isAuthenticationLoading); this.loading = this.store.select(isAuthenticationLoading);
@@ -103,7 +123,7 @@ export class LogInComponent implements OnDestroy, OnInit {
.takeWhile(() => this.alive) .takeWhile(() => this.alive)
.filter((authenticated) => authenticated) .filter((authenticated) => authenticated)
.subscribe(() => { .subscribe(() => {
this.router.navigate(['/']); this.authService.redirectToPreviousUrl();
}); });
} }
@@ -116,20 +136,13 @@ export class LogInComponent implements OnDestroy, OnInit {
} }
/** /**
* Go to the home page. * Reset error or message.
* @method home
*/ */
public home() { public resetErrorOrMessage() {
this.router.navigate(['/home']); if (this.hasError || this.hasMessage) {
}
/**
* Reset error.
*/
public resetError() {
if (this.hasError) {
this.store.dispatch(new ResetAuthenticationErrorAction()); this.store.dispatch(new ResetAuthenticationErrorAction());
this.hasError = false; this.hasError = false;
this.hasMessage = false;
} }
} }
@@ -138,7 +151,8 @@ export class LogInComponent implements OnDestroy, OnInit {
* @method register * @method register
*/ */
public register() { public register() {
this.router.navigate(['/register']); // TODO enable after registration process is done
// this.router.navigate(['/register']);
} }
/** /**

View File

@@ -2,8 +2,6 @@
<div *ngIf="!(loading | async)" class="form-login px-4 py-3"> <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> <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> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a> <button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()">{{"logout.form.submit" | translate}}</button>
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
</div> </div>

View File

@@ -75,13 +75,6 @@ export class LogOutComponent implements OnDestroy, OnInit {
this.router.navigate(['/home']); this.router.navigate(['/home']);
} }
/**
* To to the log in page.
*/
public logIn() {
this.router.navigate(['/login']);
}
public logOut() { public logOut() {
this.store.dispatch(new LogOutAction()); this.store.dispatch(new LogOutAction());
} }