mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'remotes/origin/master' into #601-resource-policies
# Conflicts: # src/app/shared/shared.module.ts
This commit is contained in:
@@ -14,6 +14,18 @@
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
<button class="btn btn-light" [disabled]="!(canReset$ | async)">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-light" [disabled]="!(canDelete$ | async)">
|
||||
<i class="fa fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||
</button>
|
||||
<button *ngIf="!isImpersonated" class="btn btn-light" [disabled]="!(canImpersonate$ | async)" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isImpersonated" class="btn btn-light" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||
</button>
|
||||
</ds-form>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
|
@@ -31,6 +31,8 @@ import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../../shared/testing/auth-service.stub';
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
@@ -40,6 +42,7 @@ describe('EPersonFormComponent', () => {
|
||||
|
||||
let mockEPeople;
|
||||
let ePersonDataServiceStub: any;
|
||||
let authService: AuthServiceStub;
|
||||
|
||||
beforeEach(async(() => {
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
@@ -104,6 +107,7 @@ describe('EPersonFormComponent', () => {
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
authService = new AuthServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -125,6 +129,7 @@ describe('EPersonFormComponent', () => {
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
EPeopleRegistryComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -228,4 +233,40 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('impersonate', () => {
|
||||
let ePersonId;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'impersonate').and.callThrough();
|
||||
ePersonId = 'testEPersonId';
|
||||
component.epersonInitial = Object.assign(new EPerson(), {
|
||||
id: ePersonId
|
||||
});
|
||||
component.impersonate();
|
||||
});
|
||||
|
||||
it('should call authService.impersonate', () => {
|
||||
expect(authService.impersonate).toHaveBeenCalledWith(ePersonId);
|
||||
});
|
||||
|
||||
it('should set isImpersonated to true', () => {
|
||||
expect(component.isImpersonated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopImpersonating', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'stopImpersonatingAndRefresh').and.callThrough();
|
||||
component.stopImpersonating();
|
||||
});
|
||||
|
||||
it('should call authService.stopImpersonatingAndRefresh', () => {
|
||||
expect(authService.stopImpersonatingAndRefresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set isImpersonated to false', () => {
|
||||
expect(component.isImpersonated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -7,7 +7,7 @@ import {
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription, combineLatest } from 'rxjs';
|
||||
import { Subscription, combineLatest, of } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
@@ -22,6 +22,7 @@ import { hasValue } from '../../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
@@ -105,6 +106,24 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to reset the EPerson's password
|
||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||
*/
|
||||
canReset$: Observable<boolean> = of(false);
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to delete the EPerson
|
||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||
*/
|
||||
canDelete$: Observable<boolean> = of(false);
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to impersonate the EPerson
|
||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return true)
|
||||
*/
|
||||
canImpersonate$: Observable<boolean> = of(true);
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
@@ -129,13 +148,22 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
epersonInitial: EPerson;
|
||||
|
||||
/**
|
||||
* Whether or not this EPerson is currently being impersonated
|
||||
*/
|
||||
isImpersonated = false;
|
||||
|
||||
constructor(public epersonService: EPersonDataService,
|
||||
public groupsDataService: GroupDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,) {
|
||||
private notificationsService: NotificationsService,
|
||||
private authService: AuthService) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
if (hasValue(eperson)) {
|
||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -364,6 +392,22 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start impersonating the EPerson
|
||||
*/
|
||||
impersonate() {
|
||||
this.authService.impersonate(this.epersonInitial.id);
|
||||
this.isImpersonated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating the EPerson
|
||||
*/
|
||||
stopImpersonating() {
|
||||
this.authService.stopImpersonatingAndRefresh();
|
||||
this.isImpersonated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
|
@@ -66,6 +66,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'reload/:rnd', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||
|
@@ -402,10 +402,10 @@ export class RetrieveAuthenticatedEpersonAction implements Action {
|
||||
*/
|
||||
export class RetrieveAuthenticatedEpersonSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS;
|
||||
payload: EPerson;
|
||||
payload: string;
|
||||
|
||||
constructor(user: EPerson) {
|
||||
this.payload = user ;
|
||||
constructor(userId: string) {
|
||||
this.payload = userId ;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -235,7 +235,7 @@ describe('AuthEffects', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock) });
|
||||
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id) });
|
||||
|
||||
expect(authEffects.retrieveAuthenticatedEperson$).toBeObservable(expected);
|
||||
});
|
||||
|
@@ -43,6 +43,7 @@ import {
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
RetrieveTokenAction
|
||||
} from './auth.actions';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
|
||||
@Injectable()
|
||||
export class AuthEffects {
|
||||
@@ -97,8 +98,15 @@ export class AuthEffects {
|
||||
public retrieveAuthenticatedEperson$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON),
|
||||
switchMap((action: RetrieveAuthenticatedEpersonAction) => {
|
||||
return this.authService.retrieveAuthenticatedUserByHref(action.payload).pipe(
|
||||
map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user)),
|
||||
const impersonatedUserID = this.authService.getImpersonateID();
|
||||
let user$: Observable<EPerson>;
|
||||
if (hasValue(impersonatedUserID)) {
|
||||
user$ = this.authService.retrieveAuthenticatedUserById(impersonatedUserID);
|
||||
} else {
|
||||
user$ = this.authService.retrieveAuthenticatedUserByHref(action.payload);
|
||||
}
|
||||
return user$.pipe(
|
||||
map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)),
|
||||
catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error))));
|
||||
})
|
||||
);
|
||||
@@ -193,6 +201,7 @@ export class AuthEffects {
|
||||
.pipe(
|
||||
ofType(AuthActionTypes.LOG_OUT),
|
||||
switchMap(() => {
|
||||
this.authService.stopImpersonating();
|
||||
return this.authService.logout().pipe(
|
||||
map((value) => new LogOutSuccessAction()),
|
||||
catchError((error) => observableOf(new LogOutErrorAction(error)))
|
||||
|
@@ -18,7 +18,7 @@ import { AppState } from '../../app.reducer';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
||||
import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -235,8 +235,16 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
});
|
||||
// Get the auth header from the service.
|
||||
authorization = authService.buildAuthHeader(token);
|
||||
let newHeaders = req.headers.set('authorization', authorization);
|
||||
|
||||
// When present, add the ID of the EPerson we're impersonating to the headers
|
||||
const impersonatingID = authService.getImpersonateID();
|
||||
if (hasValue(impersonatingID)) {
|
||||
newHeaders = newHeaders.set('X-On-Behalf-Of', impersonatingID);
|
||||
}
|
||||
|
||||
// Clone the request to add the new header.
|
||||
newReq = req.clone({ headers: req.headers.set('authorization', authorization) });
|
||||
newReq = req.clone({ headers: newHeaders });
|
||||
} else {
|
||||
newReq = req.clone();
|
||||
}
|
||||
|
@@ -189,7 +189,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
|
||||
const action = new LogOutAction();
|
||||
@@ -206,7 +206,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
|
||||
const action = new LogOutSuccessAction();
|
||||
@@ -219,7 +219,7 @@ describe('authReducer', () => {
|
||||
loading: false,
|
||||
info: undefined,
|
||||
refreshing: false,
|
||||
user: undefined
|
||||
userId: undefined
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -232,7 +232,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
|
||||
const action = new LogOutErrorAction(mockError);
|
||||
@@ -244,7 +244,7 @@ describe('authReducer', () => {
|
||||
error: 'Test error message',
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -258,7 +258,7 @@ describe('authReducer', () => {
|
||||
loading: true,
|
||||
info: undefined
|
||||
};
|
||||
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock);
|
||||
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: true,
|
||||
@@ -267,7 +267,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -301,7 +301,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||
const action = new RefreshTokenAction(newTokenInfo);
|
||||
@@ -313,7 +313,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock,
|
||||
userId: EPersonMock.id,
|
||||
refreshing: true
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
@@ -327,7 +327,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock,
|
||||
userId: EPersonMock.id,
|
||||
refreshing: true
|
||||
};
|
||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||
@@ -340,7 +340,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock,
|
||||
userId: EPersonMock.id,
|
||||
refreshing: false
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
@@ -354,7 +354,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock,
|
||||
userId: EPersonMock.id,
|
||||
refreshing: true
|
||||
};
|
||||
const action = new RefreshTokenErrorAction();
|
||||
@@ -367,7 +367,7 @@ describe('authReducer', () => {
|
||||
loading: false,
|
||||
info: undefined,
|
||||
refreshing: false,
|
||||
user: undefined
|
||||
userId: undefined
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -380,7 +380,7 @@ describe('authReducer', () => {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
|
||||
state = {
|
||||
@@ -390,7 +390,7 @@ describe('authReducer', () => {
|
||||
loading: false,
|
||||
error: undefined,
|
||||
info: 'Message',
|
||||
user: undefined
|
||||
userId: undefined
|
||||
};
|
||||
});
|
||||
|
||||
|
@@ -14,7 +14,6 @@ import {
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
// import models
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthMethodType } from './models/auth.method-type';
|
||||
@@ -49,8 +48,8 @@ export interface AuthState {
|
||||
// true when refreshing token
|
||||
refreshing?: boolean;
|
||||
|
||||
// the authenticated user
|
||||
user?: EPerson;
|
||||
// the authenticated user's id
|
||||
userId?: string;
|
||||
|
||||
// all authentication Methods enabled at the backend
|
||||
authMethods?: AuthMethod[];
|
||||
@@ -112,7 +111,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
error: undefined,
|
||||
loading: false,
|
||||
info: undefined,
|
||||
user: (action as RetrieveAuthenticatedEpersonSuccessAction).payload
|
||||
userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload
|
||||
});
|
||||
|
||||
case AuthActionTypes.AUTHENTICATE_ERROR:
|
||||
@@ -144,7 +143,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
loading: false,
|
||||
info: undefined,
|
||||
refreshing: false,
|
||||
user: undefined
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED:
|
||||
@@ -155,7 +154,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
loaded: false,
|
||||
loading: false,
|
||||
info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload,
|
||||
user: undefined
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
case AuthActionTypes.REGISTRATION:
|
||||
|
@@ -8,7 +8,7 @@ import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { authReducer, AuthState } from './auth.reducer';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthService, IMPERSONATING_COOKIE } from './auth.service';
|
||||
import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
|
||||
@@ -332,5 +332,120 @@ describe('AuthService test', () => {
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
describe('impersonate', () => {
|
||||
const userId = 'testUserId';
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'refreshAfterLogout');
|
||||
authService.impersonate(userId);
|
||||
});
|
||||
|
||||
it('should impersonate user', () => {
|
||||
expect(storage.set).toHaveBeenCalledWith(IMPERSONATING_COOKIE, userId);
|
||||
});
|
||||
|
||||
it('should call refreshAfterLogout', () => {
|
||||
expect(authService.refreshAfterLogout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopImpersonating', () => {
|
||||
beforeEach(() => {
|
||||
authService.stopImpersonating();
|
||||
});
|
||||
|
||||
it('should impersonate user', () => {
|
||||
expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopImpersonatingAndRefresh', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'refreshAfterLogout');
|
||||
authService.stopImpersonatingAndRefresh();
|
||||
});
|
||||
|
||||
it('should impersonate user', () => {
|
||||
expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE);
|
||||
});
|
||||
|
||||
it('should call refreshAfterLogout', () => {
|
||||
expect(authService.refreshAfterLogout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getImpersonateID', () => {
|
||||
beforeEach(() => {
|
||||
authService.getImpersonateID();
|
||||
});
|
||||
|
||||
it('should impersonate user', () => {
|
||||
expect(storage.get).toHaveBeenCalledWith(IMPERSONATING_COOKIE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isImpersonating', () => {
|
||||
const userId = 'testUserId';
|
||||
let result: boolean;
|
||||
|
||||
describe('when the cookie doesn\'t contain a value', () => {
|
||||
beforeEach(() => {
|
||||
result = authService.isImpersonating();
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the cookie contains a value', () => {
|
||||
beforeEach(() => {
|
||||
storage.get = jasmine.createSpy().and.returnValue(userId);
|
||||
result = authService.isImpersonating();
|
||||
});
|
||||
|
||||
it('should return true', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isImpersonatingUser', () => {
|
||||
const userId = 'testUserId';
|
||||
let result: boolean;
|
||||
|
||||
describe('when the cookie doesn\'t contain a value', () => {
|
||||
beforeEach(() => {
|
||||
result = authService.isImpersonatingUser(userId);
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the cookie contains the right value', () => {
|
||||
beforeEach(() => {
|
||||
storage.get = jasmine.createSpy().and.returnValue(userId);
|
||||
result = authService.isImpersonatingUser(userId);
|
||||
});
|
||||
|
||||
it('should return true', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the cookie contains the wrong value', () => {
|
||||
beforeEach(() => {
|
||||
storage.get = jasmine.createSpy().and.returnValue('wrongValue');
|
||||
result = authService.isImpersonatingUser(userId);
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http';
|
||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, startWith, take, withLatestFrom } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
||||
import { RouterReducerState } from '@ngrx/router-store';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { CookieAttributes } from 'js-cookie';
|
||||
@@ -14,9 +14,15 @@ import { AuthRequestService } from './auth-request.service';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
||||
import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
||||
import {
|
||||
getAuthenticatedUserId,
|
||||
getAuthenticationToken,
|
||||
getRedirectUrl,
|
||||
isAuthenticated,
|
||||
isTokenRefreshing
|
||||
} from './selectors';
|
||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||
import {
|
||||
CheckAuthenticationTokenAction,
|
||||
@@ -33,6 +39,7 @@ import { AuthMethod } from './models/auth.method';
|
||||
export const LOGIN_ROUTE = '/login';
|
||||
export const LOGOUT_ROUTE = '/logout';
|
||||
export const REDIRECT_COOKIE = 'dsRedirectUrl';
|
||||
export const IMPERSONATING_COOKIE = 'dsImpersonatingEPerson';
|
||||
|
||||
/**
|
||||
* The auth service.
|
||||
@@ -163,7 +170,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authenticated user
|
||||
* Returns the authenticated user by href
|
||||
* @returns {User}
|
||||
*/
|
||||
public retrieveAuthenticatedUserByHref(userHref: string): Observable<EPerson> {
|
||||
@@ -172,6 +179,29 @@ export class AuthService {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authenticated user by id
|
||||
* @returns {User}
|
||||
*/
|
||||
public retrieveAuthenticatedUserById(userId: string): Observable<EPerson> {
|
||||
return this.epersonService.findById(userId).pipe(
|
||||
getAllSucceededRemoteDataPayload()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authenticated user from the store
|
||||
* @returns {User}
|
||||
*/
|
||||
public getAuthenticatedUserFromStore(): Observable<EPerson> {
|
||||
return this.store.pipe(
|
||||
select(getAuthenticatedUserId),
|
||||
hasValueOperator(),
|
||||
switchMap((id: string) => this.epersonService.findById(id)),
|
||||
getAllSucceededRemoteDataPayload()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if token is present into browser storage and is valid.
|
||||
*/
|
||||
@@ -430,9 +460,9 @@ export class AuthService {
|
||||
* Refresh route navigated
|
||||
*/
|
||||
public refreshAfterLogout() {
|
||||
this.router.navigate(['/home']);
|
||||
// Hard redirect to home page, so that all state is definitely lost
|
||||
this._window.nativeWindow.location.href = '/home';
|
||||
// Hard redirect to the reload page with a unique number behind it
|
||||
// so that all state is definitely lost
|
||||
this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,4 +499,51 @@ export class AuthService {
|
||||
this.storage.remove(REDIRECT_COOKIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start impersonating EPerson
|
||||
* @param epersonId ID of the EPerson to impersonate
|
||||
*/
|
||||
impersonate(epersonId: string) {
|
||||
this.storage.set(IMPERSONATING_COOKIE, epersonId);
|
||||
this.refreshAfterLogout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating EPerson
|
||||
*/
|
||||
stopImpersonating() {
|
||||
this.storage.remove(IMPERSONATING_COOKIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating EPerson and refresh the store/ui
|
||||
*/
|
||||
stopImpersonatingAndRefresh() {
|
||||
this.stopImpersonating();
|
||||
this.refreshAfterLogout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the EPerson we're currently impersonating
|
||||
* Returns undefined if we're not impersonating anyone
|
||||
*/
|
||||
getImpersonateID(): string {
|
||||
return this.storage.get(IMPERSONATING_COOKIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we are currently impersonating an EPerson
|
||||
*/
|
||||
isImpersonating(): boolean {
|
||||
return hasValue(this.getImpersonateID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we are currently impersonating a specific EPerson
|
||||
* @param epersonId ID of the EPerson to check
|
||||
*/
|
||||
isImpersonatingUser(epersonId: string): boolean {
|
||||
return this.getImpersonateID() === epersonId;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import { createSelector } from '@ngrx/store';
|
||||
*/
|
||||
import { AuthState } from './auth.reducer';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
|
||||
/**
|
||||
* Returns the user state.
|
||||
@@ -36,12 +35,11 @@ const _isAuthenticatedLoaded = (state: AuthState) => state.loaded;
|
||||
|
||||
/**
|
||||
* Return the users state
|
||||
* NOTE: when state is REHYDRATED user object lose prototype so return always a new EPerson object
|
||||
* @function _getAuthenticatedUser
|
||||
* @function _getAuthenticatedUserId
|
||||
* @param {State} state
|
||||
* @returns {EPerson}
|
||||
* @returns {string} User ID
|
||||
*/
|
||||
const _getAuthenticatedUser = (state: AuthState) => Object.assign(new EPerson(), state.user);
|
||||
const _getAuthenticatedUserId = (state: AuthState) => state.userId;
|
||||
|
||||
/**
|
||||
* Returns the authentication error.
|
||||
@@ -119,13 +117,13 @@ const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
||||
export const getAuthenticationMethods = createSelector(getAuthState, _getAuthenticationMethods);
|
||||
|
||||
/**
|
||||
* Returns the authenticated user
|
||||
* @function getAuthenticatedUser
|
||||
* Returns the authenticated user id
|
||||
* @function getAuthenticatedUserId
|
||||
* @param {AuthState} state
|
||||
* @param {any} props
|
||||
* @return {User}
|
||||
* @return {string} User ID
|
||||
*/
|
||||
export const getAuthenticatedUser = createSelector(getAuthState, _getAuthenticatedUser);
|
||||
export const getAuthenticatedUserId = createSelector(getAuthState, _getAuthenticatedUserId);
|
||||
|
||||
/**
|
||||
* Returns the authentication error.
|
||||
|
@@ -8,6 +8,7 @@
|
||||
<ds-search-navbar></ds-search-navbar>
|
||||
<ds-lang-switch></ds-lang-switch>
|
||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||
<div class="pl-2">
|
||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
||||
aria-controls="collapsingNav"
|
||||
|
@@ -13,6 +13,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
|
||||
import { authReducer } from '../core/auth/auth.reducer';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../shared/testing/utils.test';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
describe('ProfilePageComponent', () => {
|
||||
let component: ProfilePageComponent;
|
||||
@@ -20,11 +22,13 @@ describe('ProfilePageComponent', () => {
|
||||
let user;
|
||||
let authState;
|
||||
|
||||
let authService;
|
||||
let epersonService;
|
||||
let notificationsService;
|
||||
|
||||
function init() {
|
||||
user = Object.assign(new EPerson(), {
|
||||
id: 'userId',
|
||||
groups: createSuccessfulRemoteDataObject$(createPaginatedList([]))
|
||||
});
|
||||
authState = {
|
||||
@@ -32,9 +36,12 @@ describe('ProfilePageComponent', () => {
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: new AuthTokenInfo('test_token'),
|
||||
user: user
|
||||
userId: user.id
|
||||
};
|
||||
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
getAuthenticatedUserFromStore: of(user)
|
||||
});
|
||||
epersonService = jasmine.createSpyObj('epersonService', {
|
||||
findById: createSuccessfulRemoteDataObject$(user)
|
||||
});
|
||||
@@ -52,7 +59,8 @@ describe('ProfilePageComponent', () => {
|
||||
imports: [StoreModule.forRoot(authReducer), TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: EPersonDataService, useValue: epersonService },
|
||||
{ provide: NotificationsService, useValue: notificationsService }
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: AuthService, useValue: authService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { getAuthenticatedUser } from '../core/auth/selectors';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component';
|
||||
import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
@@ -13,9 +10,10 @@ import { RemoteData } from '../core/data/remote-data';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { filter, switchMap, tap } from 'rxjs/operators';
|
||||
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-profile-page',
|
||||
@@ -50,15 +48,14 @@ export class ProfilePageComponent implements OnInit {
|
||||
*/
|
||||
NOTIFICATIONS_PREFIX = 'profile.notifications.';
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
constructor(private authService: AuthService,
|
||||
private notificationsService: NotificationsService,
|
||||
private translate: TranslateService,
|
||||
private epersonService: EPersonDataService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.user$ = this.store.pipe(
|
||||
select(getAuthenticatedUser),
|
||||
this.user$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
||||
filter((user: EPerson) => hasValue(user.id)),
|
||||
switchMap((user: EPerson) => this.epersonService.findById(user.id, followLink('groups'))),
|
||||
getAllSucceededRemoteData(),
|
||||
|
@@ -14,6 +14,7 @@ import { HostWindowService } from '../host-window.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
|
||||
describe('AuthNavMenuComponent', () => {
|
||||
|
||||
@@ -25,9 +26,19 @@ describe('AuthNavMenuComponent', () => {
|
||||
let notAuthState: AuthState;
|
||||
let authState: AuthState;
|
||||
|
||||
let authService: AuthService;
|
||||
|
||||
let routerState = {
|
||||
url: '/home'
|
||||
};
|
||||
|
||||
function serviceInit() {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
getAuthenticatedUserFromStore: of(EPersonMock),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
notAuthState = {
|
||||
authenticated: false,
|
||||
@@ -39,13 +50,14 @@ describe('AuthNavMenuComponent', () => {
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: new AuthTokenInfo('test_token'),
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
}
|
||||
describe('when is a not mobile view', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
const window = new HostWindowServiceStub(800);
|
||||
serviceInit();
|
||||
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
@@ -64,12 +76,7 @@ describe('AuthNavMenuComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: HostWindowService, useValue: window },
|
||||
{
|
||||
provide: AuthService, useValue: {
|
||||
setRedirectUrl: () => { /*empty*/
|
||||
}
|
||||
}
|
||||
}
|
||||
{ provide: AuthService, useValue: authService }
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
@@ -244,6 +251,7 @@ describe('AuthNavMenuComponent', () => {
|
||||
describe('when is a mobile view', () => {
|
||||
beforeEach(async(() => {
|
||||
const window = new HostWindowServiceStub(300);
|
||||
serviceInit();
|
||||
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
@@ -262,12 +270,7 @@ describe('AuthNavMenuComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: HostWindowService, useValue: window },
|
||||
{
|
||||
provide: AuthService, useValue: {
|
||||
setRedirectUrl: () => { /*empty*/
|
||||
}
|
||||
}
|
||||
}
|
||||
{ provide: AuthService, useValue: authService }
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
|
@@ -9,9 +9,9 @@ import { fadeInOut, fadeOut } from '../animations/fade';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||
import { isNotUndefined } from '../empty.util';
|
||||
import { getAuthenticatedUser, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
|
||||
import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-auth-nav-menu',
|
||||
@@ -41,7 +41,8 @@ export class AuthNavMenuComponent implements OnInit {
|
||||
public sub: Subscription;
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private windowService: HostWindowService
|
||||
private windowService: HostWindowService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export class AuthNavMenuComponent implements OnInit {
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
this.user = this.store.pipe(select(getAuthenticatedUser));
|
||||
this.user = this.authService.getAuthenticatedUserFromStore();
|
||||
|
||||
this.showAuth = this.store.pipe(
|
||||
select(routerStateSelector),
|
||||
|
@@ -12,6 +12,8 @@ import { AppState, storeModuleConfig } from '../../../app.reducer';
|
||||
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('UserMenuComponent', () => {
|
||||
|
||||
@@ -20,6 +22,13 @@ describe('UserMenuComponent', () => {
|
||||
let deUserMenu: DebugElement;
|
||||
let authState: AuthState;
|
||||
let authStateLoading: AuthState;
|
||||
let authService: AuthService;
|
||||
|
||||
function serviceInit() {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
getAuthenticatedUserFromStore: of(EPersonMock)
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
authState = {
|
||||
@@ -27,18 +36,19 @@ describe('UserMenuComponent', () => {
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: new AuthTokenInfo('test_token'),
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
authStateLoading = {
|
||||
authenticated: true,
|
||||
loaded: true,
|
||||
loading: true,
|
||||
authToken: null,
|
||||
user: EPersonMock
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
serviceInit();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot(authReducer, {
|
||||
@@ -54,6 +64,9 @@ describe('UserMenuComponent', () => {
|
||||
}
|
||||
})
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService }
|
||||
],
|
||||
declarations: [
|
||||
UserMenuComponent
|
||||
],
|
||||
@@ -98,7 +111,7 @@ describe('UserMenuComponent', () => {
|
||||
b: true
|
||||
}));
|
||||
|
||||
expect(component.user$).toBeObservable(cold('c', {
|
||||
expect(component.user$).toBeObservable(cold('(c|)', {
|
||||
c: EPersonMock
|
||||
}));
|
||||
|
||||
@@ -137,7 +150,7 @@ describe('UserMenuComponent', () => {
|
||||
b: false
|
||||
}));
|
||||
|
||||
expect(component.user$).toBeObservable(cold('c', {
|
||||
expect(component.user$).toBeObservable(cold('(c|)', {
|
||||
c: EPersonMock
|
||||
}));
|
||||
|
||||
|
@@ -5,9 +5,10 @@ import { select, Store } from '@ngrx/store';
|
||||
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
import { getAuthenticatedUser, isAuthenticationLoading } from '../../../core/auth/selectors';
|
||||
import { isAuthenticationLoading } from '../../../core/auth/selectors';
|
||||
import { MYDSPACE_ROUTE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||
import { getProfileModulePath } from '../../../app-routing.module';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This component represents the user nav menu.
|
||||
@@ -42,7 +43,8 @@ export class UserMenuComponent implements OnInit {
|
||||
*/
|
||||
public profileRoute = getProfileModulePath();
|
||||
|
||||
constructor(private store: Store<AppState>) {
|
||||
constructor(private store: Store<AppState>,
|
||||
private authService: AuthService) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +56,7 @@ export class UserMenuComponent implements OnInit {
|
||||
this.loading$ = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set user
|
||||
this.user$ = this.store.pipe(select(getAuthenticatedUser));
|
||||
this.user$ = this.authService.getAuthenticatedUserFromStore();
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -45,6 +45,8 @@
|
||||
|
||||
</ds-dynamic-form>
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
<div *ngIf="displaySubmit">
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
|
@@ -0,0 +1,7 @@
|
||||
<ul class="navbar-nav" *ngIf="(isAuthenticated$ | async) && isImpersonating">
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-sm btn-dark" ngbTooltip="{{'nav.stop-impersonating' | translate}}" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
@@ -0,0 +1,80 @@
|
||||
import { ImpersonateNavbarComponent } from './impersonate-navbar.component';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { VarDirective } from '../utils/var.directive';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { authReducer, AuthState } from '../../core/auth/auth.reducer';
|
||||
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
import { EPersonMock } from '../testing/eperson.mock';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('ImpersonateNavbarComponent', () => {
|
||||
let component: ImpersonateNavbarComponent;
|
||||
let fixture: ComponentFixture<ImpersonateNavbarComponent>;
|
||||
let authService: AuthService;
|
||||
let authState: AuthState;
|
||||
|
||||
beforeEach(async(() => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isImpersonating: false,
|
||||
stopImpersonatingAndRefresh: {}
|
||||
});
|
||||
authState = {
|
||||
authenticated: true,
|
||||
loaded: true,
|
||||
loading: false,
|
||||
authToken: new AuthTokenInfo('test_token'),
|
||||
userId: EPersonMock.id
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ImpersonateNavbarComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), StoreModule.forRoot(authReducer)],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authState;
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(ImpersonateNavbarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('when the user is impersonating another user', () => {
|
||||
beforeEach(() => {
|
||||
component.isImpersonating = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display a button', () => {
|
||||
const button = fixture.debugElement.query(By.css('button'));
|
||||
expect(button).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should call authService\'s stopImpersonatingAndRefresh upon clicking the button', () => {
|
||||
const button = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||
button.click();
|
||||
expect(authService.stopImpersonatingAndRefresh).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is not impersonating another user', () => {
|
||||
it('should not display a button', () => {
|
||||
const button = fixture.debugElement.query(By.css('button'));
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,42 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { isAuthenticated } from '../../core/auth/selectors';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-impersonate-navbar',
|
||||
templateUrl: 'impersonate-navbar.component.html'
|
||||
})
|
||||
/**
|
||||
* Navbar component for actions to take concerning impersonating users
|
||||
*/
|
||||
export class ImpersonateNavbarComponent implements OnInit {
|
||||
/**
|
||||
* Whether or not the user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
isAuthenticated$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Is the user currently impersonating another user?
|
||||
*/
|
||||
isImpersonating: boolean;
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private authService: AuthService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
|
||||
this.isImpersonating = this.authService.isImpersonating();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating the user
|
||||
*/
|
||||
stopImpersonating() {
|
||||
this.authService.stopImpersonatingAndRefresh();
|
||||
}
|
||||
}
|
@@ -193,6 +193,7 @@ import { ModifyItemOverviewComponent } from '../+item-page/edit-item-page/modify
|
||||
import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component';
|
||||
import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive';
|
||||
import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component';
|
||||
import { ImpersonateNavbarComponent } from './impersonate-navbar/impersonate-navbar.component';
|
||||
import { ResourcePoliciesComponent } from './resource-policies/resource-policies.component';
|
||||
import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive';
|
||||
import { ResourcePolicyFormComponent } from './resource-policies/form/resource-policy-form.component';
|
||||
@@ -380,6 +381,7 @@ const COMPONENTS = [
|
||||
PublicationSearchResultListElementComponent,
|
||||
ItemVersionsNoticeComponent,
|
||||
ModifyItemOverviewComponent,
|
||||
ImpersonateNavbarComponent,
|
||||
ResourcePoliciesComponent,
|
||||
ResourcePolicyFormComponent,
|
||||
EpersonGroupListComponent,
|
||||
|
@@ -5,6 +5,7 @@ import { EPersonMock } from './eperson.mock';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||
import { hasValue } from '../empty.util';
|
||||
|
||||
export const authMethodsMock = [
|
||||
new AuthMethod('password'),
|
||||
@@ -14,6 +15,7 @@ export const authMethodsMock = [
|
||||
export class AuthServiceStub {
|
||||
|
||||
token: AuthTokenInfo = new AuthTokenInfo('token_test');
|
||||
impersonating: string;
|
||||
private _tokenExpired = false;
|
||||
private redirectUrl;
|
||||
|
||||
@@ -47,6 +49,10 @@ export class AuthServiceStub {
|
||||
return observableOf(EPersonMock);
|
||||
}
|
||||
|
||||
public retrieveAuthenticatedUserById(id: string): Observable<EPerson> {
|
||||
return observableOf(EPersonMock);
|
||||
}
|
||||
|
||||
public buildAuthHeader(token?: AuthTokenInfo): string {
|
||||
return `Bearer ${token.accessToken}`;
|
||||
}
|
||||
@@ -120,4 +126,28 @@ export class AuthServiceStub {
|
||||
retrieveAuthMethodsFromAuthStatus(status: AuthStatus) {
|
||||
return observableOf(authMethodsMock);
|
||||
}
|
||||
|
||||
impersonate(id: string) {
|
||||
this.impersonating = id;
|
||||
}
|
||||
|
||||
isImpersonating() {
|
||||
return hasValue(this.impersonating);
|
||||
}
|
||||
|
||||
isImpersonatingUser(id: string) {
|
||||
return this.impersonating === id;
|
||||
}
|
||||
|
||||
stopImpersonating() {
|
||||
this.impersonating = undefined;
|
||||
}
|
||||
|
||||
stopImpersonatingAndRefresh() {
|
||||
this.stopImpersonating();
|
||||
}
|
||||
|
||||
getImpersonateID() {
|
||||
return this.impersonating;
|
||||
}
|
||||
}
|
||||
|
@@ -170,6 +170,14 @@
|
||||
|
||||
|
||||
|
||||
"admin.access-control.epeople.actions.delete": "Delete EPerson",
|
||||
|
||||
"admin.access-control.epeople.actions.impersonate": "Impersonate EPerson",
|
||||
|
||||
"admin.access-control.epeople.actions.reset": "Reset password",
|
||||
|
||||
"admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson",
|
||||
|
||||
"admin.access-control.epeople.title": "DSpace Angular :: EPeople",
|
||||
|
||||
"admin.access-control.epeople.head": "EPeople",
|
||||
@@ -1821,6 +1829,8 @@
|
||||
|
||||
"nav.statistics.header": "Statistics",
|
||||
|
||||
"nav.stop-impersonating": "Stop impersonating EPerson",
|
||||
|
||||
|
||||
|
||||
"orgunit.listelement.badge": "Organizational Unit",
|
||||
|
Reference in New Issue
Block a user