mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 11:33:04 +00:00
Improvement for authentication module
This commit is contained in:
@@ -12,7 +12,9 @@ import { Store } from '@ngrx/store';
|
||||
export class HomePageComponent implements OnInit {
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
|
||||
constructor(private store: Store<AppState>) {}
|
||||
constructor(private store: Store<AppState>) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// set loading
|
||||
this.isAuthenticated = this.store.select(isAuthenticated);
|
||||
|
@@ -1,14 +1,18 @@
|
||||
|
||||
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';
|
||||
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
||||
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
||||
import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model';
|
||||
|
||||
export class AuthObjectFactory {
|
||||
public static getConstructor(type): GenericConstructor<DSpaceObject> {
|
||||
public static getConstructor(type): GenericConstructor<NormalizedDSpaceObject> {
|
||||
switch (type) {
|
||||
case AuthType.Eperson: {
|
||||
return NormalizedEpersonModel
|
||||
}
|
||||
|
||||
case AuthType.Status: {
|
||||
return AuthStatus
|
||||
return NormalizedAuthStatus
|
||||
}
|
||||
|
||||
default: {
|
||||
|
@@ -6,7 +6,7 @@ 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 { AuthGetRequest, 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';
|
||||
@@ -28,16 +28,16 @@ export class AuthRequestService extends HALEndpointService {
|
||||
super();
|
||||
}
|
||||
|
||||
protected submitRequest(request: RestRequest): Observable<any> {
|
||||
protected fetchRequest(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`))),
|
||||
Observable.throw(new Error(response.errorMessage))),
|
||||
successResponse
|
||||
.filter((response: AuthSuccessResponse) => isNotEmpty(response))
|
||||
.map((response: AuthSuccessResponse) => response.authResponse)
|
||||
.map((response: AuthSuccessResponse) => response.response)
|
||||
.distinctUntilChanged());
|
||||
}
|
||||
|
||||
@@ -51,8 +51,19 @@ export class AuthRequestService extends HALEndpointService {
|
||||
.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))
|
||||
.do((request: PostRequest) => this.requestService.configure(request, true))
|
||||
.flatMap((request: PostRequest) => this.fetchRequest(request))
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
|
||||
public getRequest(method: string, options?: HttpOptions): Observable<any> {
|
||||
return this.getEndpoint()
|
||||
.filter((href: string) => isNotEmpty(href))
|
||||
.map((endpointURL) => this.getEndpointByMethod(endpointURL, method))
|
||||
.distinctUntilChanged()
|
||||
.map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options))
|
||||
.do((request: PostRequest) => this.requestService.configure(request, true))
|
||||
.flatMap((request: PostRequest) => this.fetchRequest(request))
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ import { Inject, Injectable } from '@angular/core';
|
||||
import { AuthObjectFactory } from './auth-object-factory';
|
||||
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
||||
import {
|
||||
AuthErrorResponse,
|
||||
AuthStatusResponse,
|
||||
AuthSuccessResponse, ConfigSuccessResponse, ErrorResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
@@ -11,10 +13,15 @@ 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 { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { ResponseParsingService } from '../data/parsing.service';
|
||||
import { RestRequest } from '../data/request.models';
|
||||
import { AuthType } from './auth-type';
|
||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
|
||||
@Injectable()
|
||||
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||
@@ -29,19 +36,14 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
|
||||
}
|
||||
|
||||
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));
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
|
||||
const response = this.process<AuthStatus,AuthType>(data.payload, request.href);
|
||||
return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode);
|
||||
} else if (isEmpty(data.payload) && isNotEmpty(data.headers.get('authorization')) && data.statusCode === '200') {
|
||||
return new AuthSuccessResponse(new AuthTokenInfo(data.headers.get('authorization')), data.statusCode);
|
||||
} 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)
|
||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
34
src/app/core/auth/auth-storage.service.ts
Normal file
34
src/app/core/auth/auth-storage.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
/**
|
||||
* The auth service.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthStorageService {
|
||||
|
||||
constructor(@Inject(PLATFORM_ID) private platformId: string) {}
|
||||
|
||||
public get(key: string): any {
|
||||
let item = null;
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
item = JSON.parse(localStorage.getItem(key));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
public store(key: string, item: any) {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
localStorage.setItem(key, JSON.stringify(item));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public remove(key: string) {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
export enum AuthType {
|
||||
Eperson = 'eperson',
|
||||
Status = 'status'
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { type } from '../../shared/ngrx/type';
|
||||
|
||||
// import models
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
|
||||
export const AuthActionTypes = {
|
||||
AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
|
||||
@@ -49,9 +50,9 @@ export class AuthenticateAction implements Action {
|
||||
*/
|
||||
export class AuthenticatedAction implements Action {
|
||||
public type: string = AuthActionTypes.AUTHENTICATED;
|
||||
payload: string;
|
||||
payload: AuthTokenInfo;
|
||||
|
||||
constructor(token: string) {
|
||||
constructor(token: AuthTokenInfo) {
|
||||
this.payload = token;
|
||||
}
|
||||
}
|
||||
@@ -108,10 +109,10 @@ export class AuthenticationErrorAction implements Action {
|
||||
*/
|
||||
export class AuthenticationSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.AUTHENTICATE_SUCCESS;
|
||||
payload: Eperson;
|
||||
payload: AuthTokenInfo;
|
||||
|
||||
constructor(user: Eperson) {
|
||||
this.payload = user;
|
||||
constructor(token: AuthTokenInfo) {
|
||||
this.payload = token;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -23,23 +23,7 @@ import {
|
||||
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
|
||||
*/
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
|
||||
@Injectable()
|
||||
export class AuthEffects {
|
||||
@@ -51,18 +35,29 @@ export class AuthEffects {
|
||||
@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))
|
||||
.map((response: AuthStatus) => new AuthenticationSuccessAction(response.token))
|
||||
.catch((error) => Observable.of(new AuthenticationErrorAction(error)));
|
||||
});
|
||||
|
||||
// It means "reacts to this action but don't send another"
|
||||
@Effect()
|
||||
public authenticateSuccess: Observable<Action> = this.actions$
|
||||
.ofType(AuthActionTypes.AUTHENTICATE_SUCCESS)
|
||||
.do((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload))
|
||||
.map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||
|
||||
@Effect({dispatch: false})
|
||||
public logOutSuccess: Observable<Action> = this.actions$
|
||||
.ofType(AuthActionTypes.LOG_OUT_SUCCESS)
|
||||
.do((action: LogOutSuccessAction) => this.authService.removeToken());
|
||||
|
||||
@Effect()
|
||||
public authenticated: Observable<Action> = this.actions$
|
||||
.ofType(AuthActionTypes.AUTHENTICATED)
|
||||
.switchMap((action: AuthenticatedAction) => {
|
||||
return this.authService.authenticatedUser()
|
||||
return this.authService.authenticatedUser(action.payload)
|
||||
.map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), user))
|
||||
.catch((error) => Observable.of(new AuthenticatedErrorAction(error)));
|
||||
});
|
||||
@@ -70,7 +65,7 @@ export class AuthEffects {
|
||||
@Effect()
|
||||
public createUser: Observable<Action> = this.actions$
|
||||
.ofType(AuthActionTypes.REGISTRATION)
|
||||
.debounceTime(500)
|
||||
.debounceTime(500) // to remove when functionality is implemented
|
||||
.switchMap((action: RegistrationAction) => {
|
||||
return this.authService.create(action.payload)
|
||||
.map((user: Eperson) => new RegistrationSuccessAction(user))
|
||||
|
97
src/app/core/auth/auth.interceptor.ts
Normal file
97
src/app/core/auth/auth.interceptor.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
HttpClient, HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse,
|
||||
HttpErrorResponse
|
||||
} from '@angular/common/http';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import 'rxjs/add/observable/throw'
|
||||
import 'rxjs/add/operator/catch';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthType } from './auth-type';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
constructor(private inj: Injector, private router: Router) { }
|
||||
|
||||
private isUnauthorized(status: number): boolean {
|
||||
return status === 401 || status === 403;
|
||||
}
|
||||
|
||||
private isLoginResponse(url: string): boolean {
|
||||
return url.endsWith('/authn/login');
|
||||
}
|
||||
|
||||
private isLogoutResponse(url: string): boolean {
|
||||
return url.endsWith('/authn/logout');
|
||||
}
|
||||
|
||||
private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus {
|
||||
const authStatus = new AuthStatus();
|
||||
authStatus.id = null;
|
||||
authStatus.okay = true;
|
||||
if (authenticated) {
|
||||
authStatus.authenticated = true;
|
||||
authStatus.token = new AuthTokenInfo(accessToken);
|
||||
} else {
|
||||
authStatus.authenticated = false;
|
||||
authStatus.error = JSON.parse(error);
|
||||
}
|
||||
return authStatus;
|
||||
}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
|
||||
const authService = this.inj.get(AuthService);
|
||||
|
||||
// Get the auth header from the service.
|
||||
const Authorization = authService.getAuthHeader();
|
||||
|
||||
let authReq;
|
||||
if (isNotEmpty(Authorization)) {
|
||||
// Clone the request to add the new header.
|
||||
authReq = req.clone({headers: req.headers.set('authorization', Authorization)});
|
||||
} else {
|
||||
authReq = req.clone();
|
||||
}
|
||||
|
||||
// Pass on the cloned request instead of the original request.
|
||||
return next.handle(authReq)
|
||||
.map((response) => {
|
||||
if (response instanceof HttpResponse && response.status === 200 && (this.isLoginResponse(response.url) || this.isLogoutResponse(response.url))) {
|
||||
let authRes: HttpResponse<any>;
|
||||
if (this.isLoginResponse(response.url)) {
|
||||
const token = response.headers.get('authorization');
|
||||
authRes = response.clone({body: this.makeAuthStatusObject(true, token)});
|
||||
} else {
|
||||
authRes = response.clone({body: this.makeAuthStatusObject(false)});
|
||||
}
|
||||
return authRes;
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
})
|
||||
.catch((error, caught) => {
|
||||
// Intercept an unauthorized error response
|
||||
if (error instanceof HttpErrorResponse && this.isUnauthorized(error.status)) {
|
||||
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
|
||||
const authResponse = new HttpResponse({
|
||||
body: this.makeAuthStatusObject(false, null, error.error),
|
||||
headers: error.headers,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
url: error.url
|
||||
});
|
||||
return Observable.of(authResponse);
|
||||
} else {
|
||||
// Return error response as is.
|
||||
return Observable.throw(error);
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ import {
|
||||
|
||||
// import models
|
||||
import { Eperson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
|
||||
/**
|
||||
* The auth state.
|
||||
@@ -25,6 +26,9 @@ export interface AuthState {
|
||||
// true when loading
|
||||
loading: boolean;
|
||||
|
||||
// access token
|
||||
token?: AuthTokenInfo;
|
||||
|
||||
// the authenticated user
|
||||
user?: Eperson;
|
||||
}
|
||||
@@ -62,8 +66,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
|
||||
case AuthActionTypes.AUTHENTICATED_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
authenticated: (action as AuthenticatedSuccessAction).payload.authenticated,
|
||||
authenticated: true,
|
||||
loaded: true,
|
||||
error: undefined,
|
||||
loading: false,
|
||||
user: (action as AuthenticatedSuccessAction).payload.user
|
||||
});
|
||||
|
||||
@@ -76,26 +82,26 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
});
|
||||
|
||||
case AuthActionTypes.AUTHENTICATE_SUCCESS:
|
||||
case AuthActionTypes.REGISTRATION_SUCCESS:
|
||||
const user: Eperson = (action as AuthenticationSuccessAction).payload;
|
||||
const token: AuthTokenInfo = (action as AuthenticationSuccessAction).payload;
|
||||
|
||||
// verify user is not null
|
||||
if (user === null) {
|
||||
// verify token is not null
|
||||
if (token === null) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
authenticated: true,
|
||||
error: undefined,
|
||||
loading: false,
|
||||
user: user
|
||||
token: token
|
||||
});
|
||||
|
||||
case AuthActionTypes.REGISTRATION_SUCCESS:
|
||||
return state;
|
||||
|
||||
case AuthActionTypes.RESET_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
authenticated: null,
|
||||
error: undefined,
|
||||
loaded: false,
|
||||
loading: false
|
||||
loading: false,
|
||||
});
|
||||
|
||||
case AuthActionTypes.LOG_OUT_ERROR:
|
||||
@@ -109,7 +115,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
return Object.assign({}, state, {
|
||||
authenticated: false,
|
||||
error: undefined,
|
||||
user: undefined
|
||||
loaded: false,
|
||||
loading: false,
|
||||
user: undefined,
|
||||
token: undefined
|
||||
});
|
||||
|
||||
case AuthActionTypes.REGISTRATION:
|
||||
|
@@ -6,6 +6,10 @@ 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';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { isNotEmpty, isNotNull } from '../../shared/empty.util';
|
||||
import { AuthStorageService } from './auth-storage.service';
|
||||
|
||||
export const MOCK_USER = new Eperson();
|
||||
MOCK_USER.id = '92a59227-ccf7-46da-9776-86c3fc64147f';
|
||||
@@ -30,21 +34,28 @@ MOCK_USER.metadata = [
|
||||
}
|
||||
];
|
||||
|
||||
export const TOKENITEM = 'ds-token';
|
||||
export const TOKENITEM = 'dsAuthInfo';
|
||||
|
||||
/**
|
||||
* The user service.
|
||||
* The auth service.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
||||
/**
|
||||
* True if authenticated
|
||||
* @type
|
||||
* @type boolean
|
||||
*/
|
||||
private _authenticated = false;
|
||||
|
||||
constructor(private authRequestService: AuthRequestService) {}
|
||||
/**
|
||||
* The url to redirect after login
|
||||
* @type string
|
||||
*/
|
||||
private _redirectUrl: string;
|
||||
|
||||
constructor(private authRequestService: AuthRequestService, private storage: AuthStorageService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
@@ -53,32 +64,28 @@ export class AuthService {
|
||||
* @param {string} password The user's password
|
||||
* @returns {Observable<User>} The authenticated user observable.
|
||||
*/
|
||||
public authenticate(user: string, password: string): Observable<Eperson> {
|
||||
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.
|
||||
// const body = `user=${user}&password=${password}`;
|
||||
// const body = encodeURI('password=test&user=vera.aloe@mailinator.com');
|
||||
// const body = [{user}, {password}];
|
||||
const formData: FormData = new FormData();
|
||||
formData.append('user', user);
|
||||
formData.append('password', password);
|
||||
const body = 'password=' + password.toString() + '&user=' + user.toString();
|
||||
// const body = encodeURI('password=' + password.toString() + '&user=' + user.toString());
|
||||
const body = encodeURI(`password=${password}&user=${user}`);
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
options.headers = headers;
|
||||
options.responseType = 'text';
|
||||
this.authRequestService.postToEndpoint('login', body, options)
|
||||
.subscribe((r) => {
|
||||
console.log(r);
|
||||
})
|
||||
if (user === 'test' && password === 'password') {
|
||||
this._authenticated = true;
|
||||
return Observable.of(MOCK_USER);
|
||||
return this.authRequestService.postToEndpoint('login', body, options)
|
||||
.map((status: AuthStatus) => {
|
||||
if (status.authenticated) {
|
||||
return status;
|
||||
} else {
|
||||
throw(new Error('Invalid email or password'));
|
||||
}
|
||||
})
|
||||
|
||||
return Observable.throw(new Error('Invalid email or password'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,11 +100,25 @@ export class AuthService {
|
||||
* Returns the authenticated user
|
||||
* @returns {User}
|
||||
*/
|
||||
public authenticatedUser(): Observable<Eperson> {
|
||||
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
|
||||
// Normally you would do an HTTP request to determine if
|
||||
// the user has an existing auth session on the server
|
||||
// but, let's just return the mock user for this example.
|
||||
return Observable.of(MOCK_USER);
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||
options.headers = headers;
|
||||
return this.authRequestService.getRequest('status', options)
|
||||
.map((status: AuthStatus) => {
|
||||
if (status.authenticated) {
|
||||
this._authenticated = true;
|
||||
return status.eperson[0];
|
||||
} else {
|
||||
this._authenticated = false;
|
||||
throw(new Error('Not authenticated'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +140,47 @@ export class AuthService {
|
||||
public signout(): Observable<boolean> {
|
||||
// Normally you would do an HTTP request sign end the session
|
||||
// but, let's just return an observable of true.
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
|
||||
const options: HttpOptions = Object.create({headers, responseType: 'text'});
|
||||
return this.authRequestService.getRequest('logout', options)
|
||||
.map((status: AuthStatus) => {
|
||||
if (!status.authenticated) {
|
||||
this._authenticated = false;
|
||||
return Observable.of(true);
|
||||
return true;
|
||||
} else {
|
||||
throw(new Error('Invalid email or password'));
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public getAuthHeader(): string {
|
||||
// Retrieve authentication token info
|
||||
const token = this.storage.get(TOKENITEM);
|
||||
return (isNotNull(token) && this._authenticated) ? `Bearer ${token.accessToken}` : '';
|
||||
}
|
||||
|
||||
public getToken(): AuthTokenInfo {
|
||||
// Retrieve authentication token info
|
||||
return this.storage.get(TOKENITEM);
|
||||
}
|
||||
|
||||
public storeToken(token: AuthTokenInfo) {
|
||||
// Save authentication token info
|
||||
return this.storage.store(TOKENITEM, JSON.stringify(token));
|
||||
}
|
||||
|
||||
public removeToken() {
|
||||
// Remove authentication token info
|
||||
return this.storage.remove(TOKENITEM);
|
||||
}
|
||||
|
||||
get redirectUrl(): string {
|
||||
return this._redirectUrl;
|
||||
}
|
||||
|
||||
set redirectUrl(value: string) {
|
||||
this._redirectUrl = value;
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import { Store } from '@ngrx/store';
|
||||
// reducers
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes
|
||||
@@ -18,37 +19,44 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor(private router: Router, private store: Store<CoreState>) {}
|
||||
constructor(private authService: AuthService, private router: Router, private store: Store<CoreState>) {}
|
||||
|
||||
/**
|
||||
* True when user is authenticated
|
||||
* @method canActivate
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
|
||||
// get observable
|
||||
const observable = this.store.select(isAuthenticated);
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
const url = state.url;
|
||||
|
||||
// redirect to sign in page if user is not authenticated
|
||||
observable.subscribe((authenticated) => {
|
||||
if (!authenticated) {
|
||||
this.router.navigate(['/login']);
|
||||
return this.handleAuth(url);
|
||||
}
|
||||
});
|
||||
|
||||
return observable;
|
||||
/**
|
||||
* True when user is authenticated
|
||||
* @method canActivateChild
|
||||
*/
|
||||
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
return this.canActivate(route, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when user is authenticated
|
||||
* @method canLoad
|
||||
*/
|
||||
canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean {
|
||||
canLoad(route: Route): Observable<boolean> {
|
||||
const url = `/${route.path}`;
|
||||
|
||||
return this.handleAuth(url);
|
||||
}
|
||||
|
||||
private handleAuth(url: string): Observable<boolean> {
|
||||
// get observable
|
||||
const observable = this.store.select(isAuthenticated);
|
||||
|
||||
// redirect to sign in page if user is not authenticated
|
||||
observable.subscribe((authenticated) => {
|
||||
if (!authenticated) {
|
||||
this.authService.redirectUrl = url;
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
});
|
||||
|
7
src/app/core/auth/models/auth-error.model.ts
Normal file
7
src/app/core/auth/models/auth-error.model.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface AuthError {
|
||||
error: string,
|
||||
message: string,
|
||||
path: string,
|
||||
status: number
|
||||
timestamp: number
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
export interface AuthInfo {
|
||||
access_token?: string,
|
||||
expires?: number,
|
||||
expires_in?: number
|
||||
}
|
@@ -1,4 +1,7 @@
|
||||
import { AuthError } from './auth-error.model';
|
||||
import { AuthTokenInfo } from './auth-token-info.model';
|
||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||
import { Eperson } from '../../eperson/models/eperson.model';
|
||||
|
||||
export class AuthStatus extends DSpaceObject {
|
||||
|
||||
@@ -6,4 +9,9 @@ export class AuthStatus extends DSpaceObject {
|
||||
|
||||
authenticated: boolean;
|
||||
|
||||
error?: AuthError;
|
||||
|
||||
eperson: Eperson[];
|
||||
|
||||
token?: AuthTokenInfo
|
||||
}
|
||||
|
11
src/app/core/auth/models/auth-token-info.model.ts
Normal file
11
src/app/core/auth/models/auth-token-info.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export class AuthTokenInfo {
|
||||
public accessToken: string;
|
||||
public expires?: number;
|
||||
|
||||
constructor(token: string, expiresIn?: number) {
|
||||
this.accessToken = token.replace('Bearer ', '');
|
||||
if (expiresIn) {
|
||||
this.expires = expiresIn * 1000 + Date.now();
|
||||
}
|
||||
}
|
||||
}
|
26
src/app/core/auth/models/normalized-auth-status.model.ts
Normal file
26
src/app/core/auth/models/normalized-auth-status.model.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AuthStatus } from './auth-status.model';
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { mapsTo } from '../../cache/builders/build-decorators';
|
||||
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
|
||||
import { Eperson } from '../../eperson/models/eperson.model';
|
||||
|
||||
@mapsTo(AuthStatus)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedAuthStatus extends NormalizedDSpaceObject {
|
||||
|
||||
/**
|
||||
* True if REST API is up and running, should never return false
|
||||
*/
|
||||
@autoserialize
|
||||
okay: boolean;
|
||||
|
||||
/**
|
||||
* True if the token is valid, false if there was no token or the token wasn't valid
|
||||
*/
|
||||
@autoserialize
|
||||
authenticated: boolean;
|
||||
|
||||
@autoserializeAs(Eperson)
|
||||
eperson: Eperson[];
|
||||
|
||||
}
|
32
src/app/core/cache/response-cache.models.ts
vendored
32
src/app/core/cache/response-cache.models.ts
vendored
@@ -2,12 +2,16 @@ import { RequestError } from '../data/request.models';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||
import { ConfigObject } from '../shared/config/config.model';
|
||||
import { AuthTokenInfo } from '../auth/models/auth-token-info.model';
|
||||
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
|
||||
import { AuthStatus } from '../auth/models/auth-status.model';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class RestResponse {
|
||||
public toCache = true;
|
||||
constructor(
|
||||
public isSuccessful: boolean,
|
||||
public statusCode: string
|
||||
public statusCode: string,
|
||||
) { }
|
||||
}
|
||||
|
||||
@@ -63,11 +67,31 @@ export class ConfigSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthSuccessResponse extends RestResponse {
|
||||
export class AuthStatusResponse extends RestResponse {
|
||||
public toCache = false;
|
||||
constructor(
|
||||
public authResponse: any,
|
||||
public response: AuthStatus,
|
||||
public statusCode: string
|
||||
) {
|
||||
super(true, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthSuccessResponse extends RestResponse {
|
||||
public toCache = false;
|
||||
constructor(
|
||||
public response: AuthTokenInfo,
|
||||
public statusCode: string
|
||||
) {
|
||||
super(true, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthErrorResponse extends RestResponse {
|
||||
public toCache = false;
|
||||
constructor(
|
||||
public response: any,
|
||||
public statusCode: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
}
|
||||
|
@@ -41,6 +41,10 @@ import { UUIDService } from './shared/uuid.service';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { AuthenticatedGuard } from './auth/authenticated.guard';
|
||||
import { AuthRequestService } from './auth/auth-request.service';
|
||||
import { AuthResponseParsingService } from './auth/auth-response-parsing.service';
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { AuthInterceptor } from './auth/auth.interceptor';
|
||||
import { AuthStorageService } from './auth/auth-storage.service';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -60,7 +64,9 @@ const PROVIDERS = [
|
||||
ApiService,
|
||||
AuthenticatedGuard,
|
||||
AuthRequestService,
|
||||
AuthResponseParsingService,
|
||||
AuthService,
|
||||
AuthStorageService,
|
||||
CommunityDataService,
|
||||
CollectionDataService,
|
||||
DSOResponseParsingService,
|
||||
@@ -83,7 +89,13 @@ const PROVIDERS = [
|
||||
SubmissionFormsConfigService,
|
||||
SubmissionSectionsConfigService,
|
||||
UUIDService,
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
||||
// register TokenInterceptor as HttpInterceptor
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AuthInterceptor,
|
||||
multi: true
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -193,8 +193,8 @@ export class AuthPostRequest extends PostRequest {
|
||||
}
|
||||
|
||||
export class AuthGetRequest extends GetRequest {
|
||||
constructor(uuid: string, href: string) {
|
||||
super(uuid, href);
|
||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||
super(uuid, href, null, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
|
@@ -17,6 +17,7 @@ import { RequestConfigureAction, RequestExecuteAction } from './request.actions'
|
||||
import { GetRequest, RestRequest, RestRequestMethod } from './request.models';
|
||||
|
||||
import { RequestEntry, RequestState } from './request.reducer';
|
||||
import { ResponseCacheRemoveAction } from '../cache/response-cache.actions';
|
||||
|
||||
@Injectable()
|
||||
export class RequestService {
|
||||
@@ -66,9 +67,9 @@ export class RequestService {
|
||||
.flatMap((uuid: string) => this.getByUUID(uuid));
|
||||
}
|
||||
|
||||
configure<T extends CacheableObject>(request: RestRequest): void {
|
||||
if (request.method !== RestRequestMethod.Get || !this.isCachedOrPending(request)) {
|
||||
this.dispatchRequest(request);
|
||||
configure<T extends CacheableObject>(request: RestRequest, overrideRequest: boolean = false): void {
|
||||
if (request.method !== RestRequestMethod.Get || !this.isCachedOrPending(request) || overrideRequest) {
|
||||
this.dispatchRequest(request, overrideRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +102,10 @@ export class RequestService {
|
||||
return isCached || isPending;
|
||||
}
|
||||
|
||||
private dispatchRequest(request: RestRequest) {
|
||||
private dispatchRequest(request: RestRequest, overrideRequest: boolean) {
|
||||
this.store.dispatch(new RequestConfigureAction(request));
|
||||
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
||||
if (request.method === RestRequestMethod.Get) {
|
||||
if (request.method === RestRequestMethod.Get && !overrideRequest) {
|
||||
this.trackRequestsOnTheirWayToTheStore(request);
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
export interface DSpaceRESTV2Response {
|
||||
payload: {
|
||||
[name: string]: any;
|
||||
@@ -5,5 +7,6 @@ export interface DSpaceRESTV2Response {
|
||||
_links?: any;
|
||||
page?: any;
|
||||
},
|
||||
headers: HttpHeaders,
|
||||
statusCode: string
|
||||
}
|
||||
|
@@ -61,21 +61,16 @@ export class DSpaceRESTv2Service {
|
||||
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));
|
||||
})*/
|
||||
requestOptions.headers = Object.assign(new HttpHeaders(), options.headers);
|
||||
}
|
||||
if (options && options.responseType) {
|
||||
// requestOptions.responseType = options.responseType;
|
||||
requestOptions.responseType = options.responseType;
|
||||
}
|
||||
return this.http.request(method, url, requestOptions)
|
||||
.map((res) => ({ payload: res.body, statusCode: res.statusText }))
|
||||
.map((res) => {
|
||||
console.log(res);
|
||||
return ({ payload: res.body, headers: res.headers, statusCode: res.statusText })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Error: ', err);
|
||||
return Observable.throw(err);
|
||||
|
Reference in New Issue
Block a user