mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'master' into angular-cli
This commit is contained in:
@@ -22,6 +22,7 @@ import { EditRelationshipComponent } from './item-relationships/edit-relationshi
|
||||
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -47,6 +48,7 @@ import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.co
|
||||
ItemMetadataComponent,
|
||||
ItemRelationshipsComponent,
|
||||
ItemBitstreamsComponent,
|
||||
ItemVersionHistoryComponent,
|
||||
EditInPlaceFieldComponent,
|
||||
EditRelationshipComponent,
|
||||
EditRelationshipListComponent,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { ItemPageResolver } from '../item-page.resolver';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { EditItemPageComponent } from './edit-item-page.component';
|
||||
@@ -14,6 +13,7 @@ import { ItemCollectionMapperComponent } from './item-collection-mapper/item-col
|
||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||
|
||||
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||
@@ -75,6 +75,11 @@ export const ITEM_EDIT_MOVE_PATH = 'move';
|
||||
/* TODO - change when curate page exists */
|
||||
component: ItemBitstreamsComponent,
|
||||
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||
},
|
||||
{
|
||||
path: 'versionhistory',
|
||||
component: ItemVersionHistoryComponent,
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<div class="mt-4">
|
||||
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
|
||||
</div>
|
||||
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
|
||||
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
|
||||
</div>
|
@@ -0,0 +1,44 @@
|
||||
import { ItemVersionHistoryComponent } from './item-version-history.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils';
|
||||
|
||||
describe('ItemVersionHistoryComponent', () => {
|
||||
let component: ItemVersionHistoryComponent;
|
||||
let fixture: ComponentFixture<ItemVersionHistoryComponent>;
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
uuid: 'item-identifier-1',
|
||||
handle: '123456789/1',
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ItemVersionHistoryComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) } } }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemVersionHistoryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should initialize the itemRD$ from the route\'s data', (done) => {
|
||||
component.itemRD$.subscribe((itemRD) => {
|
||||
expect(itemRD.payload).toBe(item);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,35 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-version-history',
|
||||
templateUrl: './item-version-history.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for listing and managing an item's version history
|
||||
*/
|
||||
export class ItemVersionHistoryComponent {
|
||||
/**
|
||||
* The item to display the version history for
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||
<div class="simple-view-link my-3">
|
||||
@@ -21,6 +22,7 @@
|
||||
</table>
|
||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
||||
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||
|
@@ -59,7 +59,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
|
||||
MetadataRepresentationListComponent,
|
||||
RelatedEntitiesSearchComponent,
|
||||
TabbedRelatedEntitiesSearchComponent,
|
||||
AbstractIncrementalListComponent
|
||||
AbstractIncrementalListComponent,
|
||||
],
|
||||
exports: [
|
||||
ItemComponent,
|
||||
|
@@ -28,6 +28,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
||||
followLink('owningCollection'),
|
||||
followLink('bundles'),
|
||||
followLink('relationships'),
|
||||
followLink('version', undefined, true, followLink('versionhistory')),
|
||||
).pipe(
|
||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||
);
|
||||
|
@@ -1,8 +1,10 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||
|
@@ -63,16 +63,29 @@ export function getDSOPath(dso: DSpaceObject): string {
|
||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
|
||||
{
|
||||
path: 'mydspace',
|
||||
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
|
||||
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' },
|
||||
{ path: PROFILE_MODULE_PATH, loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard] },
|
||||
{
|
||||
path: 'workspaceitems',
|
||||
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
|
||||
},
|
||||
{
|
||||
path: 'workflowitems',
|
||||
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
|
||||
},
|
||||
{
|
||||
path: PROFILE_MODULE_PATH,
|
||||
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||
],
|
||||
{
|
||||
|
@@ -9,6 +9,7 @@ import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest }
|
||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Injectable()
|
||||
export class AuthRequestService {
|
||||
@@ -16,7 +17,8 @@ export class AuthRequestService {
|
||||
protected browseEndpoint = '';
|
||||
|
||||
constructor(protected halService: HALEndpointService,
|
||||
protected requestService: RequestService) {
|
||||
protected requestService: RequestService,
|
||||
private http: HttpClient) {
|
||||
}
|
||||
|
||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||
@@ -36,7 +38,7 @@ export class AuthRequestService {
|
||||
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
|
||||
}
|
||||
|
||||
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
||||
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<any> {
|
||||
return this.halService.getEndpoint(this.linkName).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||
|
@@ -5,6 +5,8 @@ import { type } from '../../shared/ngrx/type';
|
||||
// import models
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
|
||||
export const AuthActionTypes = {
|
||||
AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
|
||||
@@ -14,12 +16,16 @@ export const AuthActionTypes = {
|
||||
AUTHENTICATED_ERROR: type('dspace/auth/AUTHENTICATED_ERROR'),
|
||||
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
|
||||
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
|
||||
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
|
||||
CHECK_AUTHENTICATION_TOKEN_COOKIE: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_COOKIE'),
|
||||
RETRIEVE_AUTH_METHODS: type('dspace/auth/RETRIEVE_AUTH_METHODS'),
|
||||
RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'),
|
||||
RETRIEVE_AUTH_METHODS_ERROR: type('dspace/auth/RETRIEVE_AUTH_METHODS_ERROR'),
|
||||
REDIRECT_TOKEN_EXPIRED: type('dspace/auth/REDIRECT_TOKEN_EXPIRED'),
|
||||
REDIRECT_AUTHENTICATION_REQUIRED: type('dspace/auth/REDIRECT_AUTHENTICATION_REQUIRED'),
|
||||
REFRESH_TOKEN: type('dspace/auth/REFRESH_TOKEN'),
|
||||
REFRESH_TOKEN_SUCCESS: type('dspace/auth/REFRESH_TOKEN_SUCCESS'),
|
||||
REFRESH_TOKEN_ERROR: type('dspace/auth/REFRESH_TOKEN_ERROR'),
|
||||
RETRIEVE_TOKEN: type('dspace/auth/RETRIEVE_TOKEN'),
|
||||
ADD_MESSAGE: type('dspace/auth/ADD_MESSAGE'),
|
||||
RESET_MESSAGES: type('dspace/auth/RESET_MESSAGES'),
|
||||
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
||||
@@ -95,7 +101,7 @@ export class AuthenticatedErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +115,7 @@ export class AuthenticationErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +144,11 @@ export class CheckAuthenticationTokenAction implements Action {
|
||||
|
||||
/**
|
||||
* Check Authentication Token Error.
|
||||
* @class CheckAuthenticationTokenErrorAction
|
||||
* @class CheckAuthenticationTokenCookieAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class CheckAuthenticationTokenErrorAction implements Action {
|
||||
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR;
|
||||
export class CheckAuthenticationTokenCookieAction implements Action {
|
||||
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +158,9 @@ export class CheckAuthenticationTokenErrorAction implements Action {
|
||||
*/
|
||||
export class LogOutAction implements Action {
|
||||
public type: string = AuthActionTypes.LOG_OUT;
|
||||
constructor(public payload?: any) {}
|
||||
|
||||
constructor(public payload?: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +173,7 @@ export class LogOutErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +184,9 @@ export class LogOutErrorAction implements Action {
|
||||
*/
|
||||
export class LogOutSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.LOG_OUT_SUCCESS;
|
||||
constructor(public payload?: any) {}
|
||||
|
||||
constructor(public payload?: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +199,7 @@ export class RedirectWhenAuthenticationIsRequiredAction implements Action {
|
||||
payload: string;
|
||||
|
||||
constructor(message: string) {
|
||||
this.payload = message ;
|
||||
this.payload = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +213,7 @@ export class RedirectWhenTokenExpiredAction implements Action {
|
||||
payload: string;
|
||||
|
||||
constructor(message: string) {
|
||||
this.payload = message ;
|
||||
this.payload = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +254,15 @@ export class RefreshTokenErrorAction implements Action {
|
||||
public type: string = AuthActionTypes.REFRESH_TOKEN_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve authentication token.
|
||||
* @class RetrieveTokenAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveTokenAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_TOKEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign up.
|
||||
* @class RegistrationAction
|
||||
@@ -268,7 +287,7 @@ export class RegistrationErrorAction implements Action {
|
||||
payload: Error;
|
||||
|
||||
constructor(payload: Error) {
|
||||
this.payload = payload ;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +328,45 @@ export class ResetAuthenticationMessagesAction implements Action {
|
||||
public type: string = AuthActionTypes.RESET_MESSAGES;
|
||||
}
|
||||
|
||||
// // Next three Actions are used by dynamic login methods
|
||||
/**
|
||||
* Action that triggers an effect fetching the authentication methods enabled ant the backend
|
||||
* @class RetrieveAuthMethodsAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveAuthMethodsAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS;
|
||||
|
||||
payload: AuthStatus;
|
||||
|
||||
constructor(authStatus: AuthStatus) {
|
||||
this.payload = authStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Authentication methods enabled at the backend
|
||||
* @class RetrieveAuthMethodsSuccessAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveAuthMethodsSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS;
|
||||
payload: AuthMethod[];
|
||||
|
||||
constructor(authMethods: AuthMethod[] ) {
|
||||
this.payload = authMethods;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password as default authentication method on error
|
||||
* @class RetrieveAuthMethodsErrorAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RetrieveAuthMethodsErrorAction implements Action {
|
||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the redirect url.
|
||||
* @class SetRedirectUrlAction
|
||||
@@ -319,7 +377,7 @@ export class SetRedirectUrlAction implements Action {
|
||||
payload: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.payload = url ;
|
||||
this.payload = url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,13 +436,21 @@ export type AuthActions
|
||||
| AuthenticationErrorAction
|
||||
| AuthenticationSuccessAction
|
||||
| CheckAuthenticationTokenAction
|
||||
| CheckAuthenticationTokenErrorAction
|
||||
| CheckAuthenticationTokenCookieAction
|
||||
| RedirectWhenAuthenticationIsRequiredAction
|
||||
| RedirectWhenTokenExpiredAction
|
||||
| RegistrationAction
|
||||
| RegistrationErrorAction
|
||||
| RegistrationSuccessAction
|
||||
| AddAuthenticationMessageAction
|
||||
| RefreshTokenAction
|
||||
| RefreshTokenErrorAction
|
||||
| RefreshTokenSuccessAction
|
||||
| ResetAuthenticationMessagesAction
|
||||
| RetrieveAuthMethodsAction
|
||||
| RetrieveAuthMethodsSuccessAction
|
||||
| RetrieveAuthMethodsErrorAction
|
||||
| RetrieveTokenAction
|
||||
| ResetAuthenticationMessagesAction
|
||||
| RetrieveAuthenticatedEpersonAction
|
||||
| RetrieveAuthenticatedEpersonErrorAction
|
||||
|
@@ -14,20 +14,26 @@ import {
|
||||
AuthenticatedSuccessAction,
|
||||
AuthenticationErrorAction,
|
||||
AuthenticationSuccessAction,
|
||||
CheckAuthenticationTokenErrorAction,
|
||||
CheckAuthenticationTokenCookieAction,
|
||||
LogOutErrorAction,
|
||||
LogOutSuccessAction,
|
||||
RefreshTokenErrorAction,
|
||||
RefreshTokenSuccessAction,
|
||||
RetrieveAuthenticatedEpersonAction,
|
||||
RetrieveAuthenticatedEpersonErrorAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsAction,
|
||||
RetrieveAuthMethodsErrorAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
RetrieveTokenAction
|
||||
} from './auth.actions';
|
||||
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||
import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthState } from './auth.reducer';
|
||||
|
||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
|
||||
describe('AuthEffects', () => {
|
||||
let authEffects: AuthEffects;
|
||||
@@ -169,13 +175,56 @@ describe('AuthEffects', () => {
|
||||
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token } });
|
||||
|
||||
const expected = cold('--b-', { b: new CheckAuthenticationTokenErrorAction() });
|
||||
const expected = cold('--b-', { b: new CheckAuthenticationTokenCookieAction() });
|
||||
|
||||
expect(authEffects.checkToken$).toBeObservable(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('checkTokenCookie$', () => {
|
||||
|
||||
describe('when check token succeeded', () => {
|
||||
it('should return a RETRIEVE_TOKEN action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is true', () => {
|
||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||
observableOf(
|
||||
{
|
||||
authenticated: true
|
||||
})
|
||||
);
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveTokenAction() });
|
||||
|
||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||
observableOf(
|
||||
{ authenticated: false })
|
||||
);
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus) });
|
||||
|
||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when check token failed', () => {
|
||||
it('should return a AUTHENTICATED_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE, payload: token } });
|
||||
|
||||
const expected = cold('--b-', { b: new AuthenticatedErrorAction(new Error('Message Error test')) });
|
||||
|
||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('retrieveAuthenticatedEperson$', () => {
|
||||
|
||||
describe('when request is successful', () => {
|
||||
@@ -232,6 +281,38 @@ describe('AuthEffects', () => {
|
||||
})
|
||||
});
|
||||
|
||||
describe('retrieveToken$', () => {
|
||||
describe('when user is authenticated', () => {
|
||||
it('should return a AUTHENTICATE_SUCCESS action in response to a RETRIEVE_TOKEN action', () => {
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
type: AuthActionTypes.RETRIEVE_TOKEN
|
||||
}
|
||||
});
|
||||
|
||||
const expected = cold('--b-', { b: new AuthenticationSuccessAction(token) });
|
||||
|
||||
expect(authEffects.retrieveToken$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is not authenticated', () => {
|
||||
it('should return a AUTHENTICATE_ERROR action in response to a RETRIEVE_TOKEN action', () => {
|
||||
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
type: AuthActionTypes.RETRIEVE_TOKEN
|
||||
}
|
||||
});
|
||||
|
||||
const expected = cold('--b-', { b: new AuthenticationErrorAction(new Error('Message Error test')) });
|
||||
|
||||
expect(authEffects.retrieveToken$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logOut$', () => {
|
||||
|
||||
describe('when refresh token succeeded', () => {
|
||||
@@ -257,4 +338,29 @@ describe('AuthEffects', () => {
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('retrieveMethods$', () => {
|
||||
|
||||
describe('when retrieve authentication methods succeeded', () => {
|
||||
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock) });
|
||||
|
||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when retrieve authentication methods failed', () => {
|
||||
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
||||
|
||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction() });
|
||||
|
||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { of as observableOf, Observable } from 'rxjs';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { filter, debounceTime, switchMap, take, tap, catchError, map } from 'rxjs/operators';
|
||||
import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// import @ngrx
|
||||
@@ -9,6 +9,14 @@ import { Action, select, Store } from '@ngrx/store';
|
||||
|
||||
// import services
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
// import actions
|
||||
import {
|
||||
AuthActionTypes,
|
||||
@@ -18,7 +26,7 @@ import {
|
||||
AuthenticatedSuccessAction,
|
||||
AuthenticationErrorAction,
|
||||
AuthenticationSuccessAction,
|
||||
CheckAuthenticationTokenErrorAction,
|
||||
CheckAuthenticationTokenCookieAction,
|
||||
LogOutErrorAction,
|
||||
LogOutSuccessAction,
|
||||
RefreshTokenAction,
|
||||
@@ -29,14 +37,12 @@ import {
|
||||
RegistrationSuccessAction,
|
||||
RetrieveAuthenticatedEpersonAction,
|
||||
RetrieveAuthenticatedEpersonErrorAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsAction,
|
||||
RetrieveAuthMethodsErrorAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
RetrieveTokenAction
|
||||
} from './auth.actions';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
|
||||
@Injectable()
|
||||
export class AuthEffects {
|
||||
@@ -47,45 +53,45 @@ export class AuthEffects {
|
||||
*/
|
||||
@Effect()
|
||||
public authenticate$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATE),
|
||||
switchMap((action: AuthenticateAction) => {
|
||||
return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
|
||||
take(1),
|
||||
map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)),
|
||||
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATE),
|
||||
switchMap((action: AuthenticateAction) => {
|
||||
return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
|
||||
take(1),
|
||||
map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)),
|
||||
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
||||
tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
|
||||
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
||||
tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
|
||||
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public authenticated$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED),
|
||||
switchMap((action: AuthenticatedAction) => {
|
||||
return this.authService.authenticatedUser(action.payload).pipe(
|
||||
map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)),
|
||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
|
||||
})
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATED),
|
||||
switchMap((action: AuthenticatedAction) => {
|
||||
return this.authService.authenticatedUser(action.payload).pipe(
|
||||
map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)),
|
||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||
);
|
||||
|
||||
// It means "reacts to this action but don't send another"
|
||||
@Effect({ dispatch: false })
|
||||
public authenticatedError$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED_ERROR),
|
||||
tap((action: LogOutSuccessAction) => this.authService.removeToken())
|
||||
);
|
||||
ofType(AuthActionTypes.AUTHENTICATED_ERROR),
|
||||
tap((action: LogOutSuccessAction) => this.authService.removeToken())
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public retrieveAuthenticatedEperson$: Observable<Action> = this.actions$.pipe(
|
||||
@@ -99,42 +105,71 @@ export class AuthEffects {
|
||||
|
||||
@Effect()
|
||||
public checkToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
|
||||
switchMap(() => {
|
||||
return this.authService.hasValidAuthenticationToken().pipe(
|
||||
map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
|
||||
catchError((error) => observableOf(new CheckAuthenticationTokenErrorAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
switchMap(() => {
|
||||
return this.authService.hasValidAuthenticationToken().pipe(
|
||||
map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
|
||||
catchError((error) => observableOf(new CheckAuthenticationTokenCookieAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public checkTokenCookie$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE),
|
||||
switchMap(() => {
|
||||
return this.authService.checkAuthenticationCookie().pipe(
|
||||
map((response: AuthStatus) => {
|
||||
if (response.authenticated) {
|
||||
return new RetrieveTokenAction();
|
||||
} else {
|
||||
return new RetrieveAuthMethodsAction(response);
|
||||
}
|
||||
}),
|
||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public createUser$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.REGISTRATION),
|
||||
debounceTime(500), // to remove when functionality is implemented
|
||||
switchMap((action: RegistrationAction) => {
|
||||
return this.authService.create(action.payload).pipe(
|
||||
map((user: EPerson) => new RegistrationSuccessAction(user)),
|
||||
catchError((error) => observableOf(new RegistrationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
ofType(AuthActionTypes.REGISTRATION),
|
||||
debounceTime(500), // to remove when functionality is implemented
|
||||
switchMap((action: RegistrationAction) => {
|
||||
return this.authService.create(action.payload).pipe(
|
||||
map((user: EPerson) => new RegistrationSuccessAction(user)),
|
||||
catchError((error) => observableOf(new RegistrationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public retrieveToken$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.RETRIEVE_TOKEN),
|
||||
switchMap((action: AuthenticateAction) => {
|
||||
return this.authService.refreshAuthenticationToken(null).pipe(
|
||||
take(1),
|
||||
map((token: AuthTokenInfo) => new AuthenticationSuccessAction(token)),
|
||||
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public refreshToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN),
|
||||
switchMap((action: RefreshTokenAction) => {
|
||||
return this.authService.refreshAuthenticationToken(action.payload).pipe(
|
||||
map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)),
|
||||
catchError((error) => observableOf(new RefreshTokenErrorAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
switchMap((action: RefreshTokenAction) => {
|
||||
return this.authService.refreshAuthenticationToken(action.payload).pipe(
|
||||
map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)),
|
||||
catchError((error) => observableOf(new RefreshTokenErrorAction()))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// It means "reacts to this action but don't send another"
|
||||
@Effect({ dispatch: false })
|
||||
public refreshTokenSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS),
|
||||
tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload))
|
||||
);
|
||||
ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS),
|
||||
tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload))
|
||||
);
|
||||
|
||||
/**
|
||||
* When the store is rehydrated in the browser,
|
||||
@@ -188,6 +223,19 @@ export class AuthEffects {
|
||||
tap(() => this.authService.redirectToLoginWhenTokenExpired())
|
||||
);
|
||||
|
||||
@Effect()
|
||||
public retrieveMethods$: Observable<Action> = this.actions$
|
||||
.pipe(
|
||||
ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS),
|
||||
switchMap((action: RetrieveAuthMethodsAction) => {
|
||||
return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload)
|
||||
.pipe(
|
||||
map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)),
|
||||
catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction()))
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Actions} actions$
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
HttpHandler,
|
||||
HttpHeaders,
|
||||
HttpInterceptor,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
@@ -17,10 +18,12 @@ 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, isUndefined, isNotNull } from '../../shared/empty.util';
|
||||
import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthMethodType } from './models/auth.method-type';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
@@ -30,17 +33,33 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
// we're creating a refresh token request list
|
||||
protected refreshTokenRequestUrls = [];
|
||||
|
||||
constructor(private inj: Injector, private router: Router, private store: Store<AppState>) { }
|
||||
constructor(private inj: Injector, private router: Router, private store: Store<AppState>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response status code is 401
|
||||
*
|
||||
* @param response
|
||||
*/
|
||||
private isUnauthorized(response: HttpResponseBase): boolean {
|
||||
// invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||
return response.status === 401;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response status code is 200 or 204
|
||||
*
|
||||
* @param response
|
||||
*/
|
||||
private isSuccess(response: HttpResponseBase): boolean {
|
||||
return (response.status === 200 || response.status === 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if http request is to authn endpoint
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http && http.url
|
||||
&& (http.url.endsWith('/authn/login')
|
||||
@@ -48,18 +67,131 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
|| http.url.endsWith('/authn/status'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response is from a login request
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isLoginResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http.url && http.url.endsWith('/authn/login');
|
||||
return http.url && http.url.endsWith('/authn/login')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response is from a logout request
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isLogoutResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http.url && http.url.endsWith('/authn/logout');
|
||||
}
|
||||
|
||||
private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string): AuthStatus {
|
||||
/**
|
||||
* Check if response is from a status request
|
||||
*
|
||||
* @param http
|
||||
*/
|
||||
private isStatusResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||
return http.url && http.url.endsWith('/authn/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract location url from the WWW-Authenticate header
|
||||
*
|
||||
* @param header
|
||||
*/
|
||||
private parseLocation(header: string): string {
|
||||
let location = header.trim();
|
||||
location = location.replace('location="', '');
|
||||
location = location.replace('"', '');
|
||||
let re = /%3A%2F%2F/g;
|
||||
location = location.replace(re, '://');
|
||||
re = /%3A/g;
|
||||
location = location.replace(re, ':');
|
||||
return location.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort authentication methods list
|
||||
*
|
||||
* @param authMethodModels
|
||||
*/
|
||||
private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] {
|
||||
const sortedAuthMethodModels: AuthMethod[] = [];
|
||||
authMethodModels.forEach((method) => {
|
||||
if (method.authMethodType === AuthMethodType.Password) {
|
||||
sortedAuthMethodModels.push(method);
|
||||
}
|
||||
});
|
||||
|
||||
authMethodModels.forEach((method) => {
|
||||
if (method.authMethodType !== AuthMethodType.Password) {
|
||||
sortedAuthMethodModels.push(method);
|
||||
}
|
||||
});
|
||||
|
||||
return sortedAuthMethodModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract authentication methods list from the WWW-Authenticate headers
|
||||
*
|
||||
* @param headers
|
||||
*/
|
||||
private parseAuthMethodsFromHeaders(headers: HttpHeaders): AuthMethod[] {
|
||||
let authMethodModels: AuthMethod[] = [];
|
||||
if (isNotEmpty(headers.get('www-authenticate'))) {
|
||||
// get the realms from the header - a realm is a single auth method
|
||||
const completeWWWauthenticateHeader = headers.get('www-authenticate');
|
||||
const regex = /(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g;
|
||||
const realms = completeWWWauthenticateHeader.match(regex);
|
||||
|
||||
// tslint:disable-next-line:forin
|
||||
for (const j in realms) {
|
||||
|
||||
const splittedRealm = realms[j].split(', ');
|
||||
const methodName = splittedRealm[0].split(' ')[0].trim();
|
||||
|
||||
let authMethodModel: AuthMethod;
|
||||
if (splittedRealm.length === 1) {
|
||||
authMethodModel = new AuthMethod(methodName);
|
||||
authMethodModels.push(authMethodModel);
|
||||
} else if (splittedRealm.length > 1) {
|
||||
let location = splittedRealm[1];
|
||||
location = this.parseLocation(location);
|
||||
authMethodModel = new AuthMethod(methodName, location);
|
||||
authMethodModels.push(authMethodModel);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the email + password login component gets rendered first
|
||||
authMethodModels = this.sortAuthMethods(authMethodModels);
|
||||
} else {
|
||||
authMethodModels.push(new AuthMethod(AuthMethodType.Password));
|
||||
}
|
||||
|
||||
return authMethodModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an AuthStatus object
|
||||
*
|
||||
* @param authenticated
|
||||
* @param accessToken
|
||||
* @param error
|
||||
* @param httpHeaders
|
||||
*/
|
||||
private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string, httpHeaders?: HttpHeaders): AuthStatus {
|
||||
const authStatus = new AuthStatus();
|
||||
// let authMethods: AuthMethodModel[];
|
||||
if (httpHeaders) {
|
||||
authStatus.authMethods = this.parseAuthMethodsFromHeaders(httpHeaders);
|
||||
}
|
||||
|
||||
authStatus.id = null;
|
||||
|
||||
authStatus.okay = true;
|
||||
// authStatus.authMethods = authMethods;
|
||||
|
||||
if (authenticated) {
|
||||
authStatus.authenticated = true;
|
||||
authStatus.token = new AuthTokenInfo(accessToken);
|
||||
@@ -70,12 +202,18 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
return authStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept method
|
||||
* @param req
|
||||
* @param next
|
||||
*/
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
|
||||
const authService = this.inj.get(AuthService);
|
||||
|
||||
const token = authService.getToken();
|
||||
let newReq;
|
||||
const token: AuthTokenInfo = authService.getToken();
|
||||
let newReq: HttpRequest<any>;
|
||||
let authorization: string;
|
||||
|
||||
if (authService.isTokenExpired()) {
|
||||
authService.setRedirectUrl(this.router.url);
|
||||
@@ -96,30 +234,41 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
}
|
||||
});
|
||||
// Get the auth header from the service.
|
||||
const Authorization = authService.buildAuthHeader(token);
|
||||
authorization = authService.buildAuthHeader(token);
|
||||
// Clone the request to add the new header.
|
||||
newReq = req.clone({headers: req.headers.set('authorization', Authorization)});
|
||||
newReq = req.clone({ headers: req.headers.set('authorization', authorization) });
|
||||
} else {
|
||||
newReq = req;
|
||||
newReq = req.clone();
|
||||
}
|
||||
|
||||
// Pass on the new request instead of the original request.
|
||||
return next.handle(newReq).pipe(
|
||||
// tap((response) => console.log('next.handle: ', response)),
|
||||
map((response) => {
|
||||
// Intercept a Login/Logout response
|
||||
if (response instanceof HttpResponse && this.isSuccess(response) && (this.isLoginResponse(response) || this.isLogoutResponse(response))) {
|
||||
if (response instanceof HttpResponse && this.isSuccess(response) && this.isAuthRequest(response)) {
|
||||
// It's a success Login/Logout response
|
||||
let authRes: HttpResponse<any>;
|
||||
if (this.isLoginResponse(response)) {
|
||||
// login successfully
|
||||
const newToken = response.headers.get('authorization');
|
||||
authRes = response.clone({body: this.makeAuthStatusObject(true, newToken)});
|
||||
authRes = response.clone({
|
||||
body: this.makeAuthStatusObject(true, newToken)
|
||||
});
|
||||
|
||||
// clean eventually refresh Requests list
|
||||
this.refreshTokenRequestUrls = [];
|
||||
} else if (this.isStatusResponse(response)) {
|
||||
authRes = response.clone({
|
||||
body: Object.assign(response.body, {
|
||||
authMethods: this.parseAuthMethodsFromHeaders(response.headers)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// logout successfully
|
||||
authRes = response.clone({body: this.makeAuthStatusObject(false)});
|
||||
authRes = response.clone({
|
||||
body: this.makeAuthStatusObject(false)
|
||||
});
|
||||
}
|
||||
return authRes;
|
||||
} else {
|
||||
@@ -129,13 +278,15 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
catchError((error, caught) => {
|
||||
// Intercept an error response
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
|
||||
// Checks if is a response from a request to an authentication endpoint
|
||||
if (this.isAuthRequest(error)) {
|
||||
// clean eventually refresh Requests list
|
||||
this.refreshTokenRequestUrls = [];
|
||||
|
||||
// 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),
|
||||
body: this.makeAuthStatusObject(false, null, error.error, error.headers),
|
||||
headers: error.headers,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
AuthenticationErrorAction,
|
||||
AuthenticationSuccessAction,
|
||||
CheckAuthenticationTokenAction,
|
||||
CheckAuthenticationTokenErrorAction,
|
||||
CheckAuthenticationTokenCookieAction,
|
||||
LogOutAction,
|
||||
LogOutErrorAction,
|
||||
LogOutSuccessAction,
|
||||
@@ -17,11 +17,19 @@ import {
|
||||
RefreshTokenAction,
|
||||
RefreshTokenErrorAction,
|
||||
RefreshTokenSuccessAction,
|
||||
ResetAuthenticationMessagesAction, RetrieveAuthenticatedEpersonErrorAction, RetrieveAuthenticatedEpersonSuccessAction,
|
||||
ResetAuthenticationMessagesAction,
|
||||
RetrieveAuthenticatedEpersonErrorAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsAction,
|
||||
RetrieveAuthMethodsErrorAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { AuthMethodType } from './models/auth.method-type';
|
||||
|
||||
describe('authReducer', () => {
|
||||
|
||||
@@ -157,18 +165,18 @@ describe('authReducer', () => {
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN_ERROR action', () => {
|
||||
it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
};
|
||||
const action = new CheckAuthenticationTokenErrorAction();
|
||||
const action = new CheckAuthenticationTokenCookieAction();
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
loading: true,
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
@@ -451,4 +459,63 @@ describe('authReducer', () => {
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
};
|
||||
const action = new RetrieveAuthMethodsAction(new AuthStatus());
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
authMethods: []
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_SUCCESS action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
authMethods: []
|
||||
};
|
||||
const authMethods = [
|
||||
new AuthMethod(AuthMethodType.Password),
|
||||
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
||||
];
|
||||
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: authMethods
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action', () => {
|
||||
initialState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: true,
|
||||
authMethods: []
|
||||
};
|
||||
|
||||
const action = new RetrieveAuthMethodsErrorAction();
|
||||
const newState = authReducer(initialState, action);
|
||||
state = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||
};
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
@@ -8,12 +8,16 @@ import {
|
||||
LogOutErrorAction,
|
||||
RedirectWhenAuthenticationIsRequiredAction,
|
||||
RedirectWhenTokenExpiredAction,
|
||||
RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RefreshTokenSuccessAction,
|
||||
RetrieveAuthenticatedEpersonSuccessAction,
|
||||
RetrieveAuthMethodsSuccessAction,
|
||||
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';
|
||||
|
||||
/**
|
||||
* The auth state.
|
||||
@@ -47,6 +51,10 @@ export interface AuthState {
|
||||
|
||||
// the authenticated user
|
||||
user?: EPerson;
|
||||
|
||||
// all authentication Methods enabled at the backend
|
||||
authMethods?: AuthMethod[];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +64,7 @@ const initialState: AuthState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -75,6 +84,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
});
|
||||
|
||||
case AuthActionTypes.AUTHENTICATED:
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
|
||||
return Object.assign({}, state, {
|
||||
loading: true
|
||||
});
|
||||
@@ -113,21 +124,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
loading: false
|
||||
});
|
||||
|
||||
case AuthActionTypes.AUTHENTICATED:
|
||||
case AuthActionTypes.AUTHENTICATE_SUCCESS:
|
||||
case AuthActionTypes.LOG_OUT:
|
||||
return state;
|
||||
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||
return Object.assign({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
loading: false
|
||||
});
|
||||
|
||||
case AuthActionTypes.LOG_OUT_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
authenticated: true,
|
||||
@@ -192,6 +192,24 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
info: undefined,
|
||||
});
|
||||
|
||||
// next three cases are used by dynamic rendering of login methods
|
||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS:
|
||||
return Object.assign({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
loading: false,
|
||||
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload
|
||||
});
|
||||
|
||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
loading: false,
|
||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||
});
|
||||
|
||||
case AuthActionTypes.SET_REDIRECT_URL:
|
||||
return Object.assign({}, state, {
|
||||
redirectUrl: (action as SetRedirectUrlAction).payload,
|
||||
|
@@ -28,6 +28,8 @@ import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
|
||||
describe('AuthService test', () => {
|
||||
|
||||
@@ -150,6 +152,26 @@ describe('AuthService test', () => {
|
||||
expect(authService.logout.bind(null)).toThrow();
|
||||
});
|
||||
|
||||
it('should return the authentication status object to check an Authentication Cookie', () => {
|
||||
authService.checkAuthenticationCookie().subscribe((status: AuthStatus) => {
|
||||
expect(status).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the authentication methods available', () => {
|
||||
const authStatus = new AuthStatus();
|
||||
|
||||
authService.retrieveAuthMethodsFromAuthStatus(authStatus).subscribe((authMethods: AuthMethod[]) => {
|
||||
expect(authMethods).toBeDefined();
|
||||
expect(authMethods.length).toBe(0);
|
||||
});
|
||||
|
||||
authStatus.authMethods = authMethodsMock;
|
||||
authService.retrieveAuthMethodsFromAuthStatus(authStatus).subscribe((authMethods: AuthMethod[]) => {
|
||||
expect(authMethods).toBeDefined();
|
||||
expect(authMethods.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
|
@@ -18,16 +18,20 @@ import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/emp
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
|
||||
import {
|
||||
CheckAuthenticationTokenAction,
|
||||
ResetAuthenticationMessagesAction,
|
||||
SetRedirectUrlAction
|
||||
} from './auth.actions';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||
import { RouteService } from '../services/route.service';
|
||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
|
||||
export const LOGIN_ROUTE = '/login';
|
||||
export const LOGOUT_ROUTE = '/logout';
|
||||
|
||||
export const REDIRECT_COOKIE = 'dsRedirectUrl';
|
||||
|
||||
/**
|
||||
@@ -114,6 +118,21 @@ export class AuthService {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if token is present into the request cookie
|
||||
*/
|
||||
public checkAuthenticationCookie(): Observable<AuthStatus> {
|
||||
// Determine if the user has an existing auth session on the server
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
options.headers = headers;
|
||||
options.withCredentials = true;
|
||||
return this.authRequestService.getRequest('status', options).pipe(
|
||||
map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the user is authenticated
|
||||
* @returns {Observable<boolean>}
|
||||
@@ -154,10 +173,10 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
||||
* Checks if token is present into browser storage and is valid.
|
||||
*/
|
||||
public checkAuthenticationToken() {
|
||||
return
|
||||
this.store.dispatch(new CheckAuthenticationTokenAction());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,8 +206,11 @@ export class AuthService {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||
if (token && token.accessToken) {
|
||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||
}
|
||||
options.headers = headers;
|
||||
options.withCredentials = true;
|
||||
return this.authRequestService.postToEndpoint('login', {}, options).pipe(
|
||||
map((status: AuthStatus) => {
|
||||
if (status.authenticated) {
|
||||
@@ -206,6 +228,18 @@ export class AuthService {
|
||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve authentication methods available
|
||||
* @returns {User}
|
||||
*/
|
||||
public retrieveAuthMethodsFromAuthStatus(status: AuthStatus): Observable<AuthMethod[]> {
|
||||
let authMethods: AuthMethod[] = [];
|
||||
if (isNotEmpty(status.authMethods)) {
|
||||
authMethods = status.authMethods;
|
||||
}
|
||||
return observableOf(authMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
* @returns {User}
|
||||
|
@@ -1,17 +1,14 @@
|
||||
|
||||
import {take} from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import {Observable, of} from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
// reducers
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { isAuthenticated, isAuthenticationLoading } from './selectors';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { AuthService } from './auth.service';
|
||||
import { RedirectWhenAuthenticationIsRequiredAction } from './auth.actions';
|
||||
import { isEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes
|
||||
|
@@ -12,6 +12,7 @@ import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||
import { AuthError } from './auth-error.model';
|
||||
import { AUTH_STATUS } from './auth-status.resource-type';
|
||||
import { AuthTokenInfo } from './auth-token-info.model';
|
||||
import { AuthMethod } from './auth.method';
|
||||
|
||||
/**
|
||||
* Object that represents the authenticated status of a user
|
||||
@@ -79,5 +80,13 @@ export class AuthStatus implements CacheableObject {
|
||||
* Authentication error if there was one for this status
|
||||
*/
|
||||
// TODO should be refactored to use the RemoteData error
|
||||
@autoserialize
|
||||
error?: AuthError;
|
||||
|
||||
/**
|
||||
* All authentication methods enabled at the backend
|
||||
*/
|
||||
@autoserialize
|
||||
authMethods: AuthMethod[];
|
||||
|
||||
}
|
||||
|
7
src/app/core/auth/models/auth.method-type.ts
Normal file
7
src/app/core/auth/models/auth.method-type.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum AuthMethodType {
|
||||
Password = 'password',
|
||||
Shibboleth = 'shibboleth',
|
||||
Ldap = 'ldap',
|
||||
Ip = 'ip',
|
||||
X509 = 'x509'
|
||||
}
|
38
src/app/core/auth/models/auth.method.ts
Normal file
38
src/app/core/auth/models/auth.method.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { AuthMethodType } from './auth.method-type';
|
||||
|
||||
export class AuthMethod {
|
||||
authMethodType: AuthMethodType;
|
||||
location?: string;
|
||||
|
||||
// isStandalonePage? = true;
|
||||
|
||||
constructor(authMethodName: string, location?: string) {
|
||||
switch (authMethodName) {
|
||||
case 'ip': {
|
||||
this.authMethodType = AuthMethodType.Ip;
|
||||
break;
|
||||
}
|
||||
case 'ldap': {
|
||||
this.authMethodType = AuthMethodType.Ldap;
|
||||
break;
|
||||
}
|
||||
case 'shibboleth': {
|
||||
this.authMethodType = AuthMethodType.Shibboleth;
|
||||
this.location = location;
|
||||
break;
|
||||
}
|
||||
case 'x509': {
|
||||
this.authMethodType = AuthMethodType.X509;
|
||||
break;
|
||||
}
|
||||
case 'password': {
|
||||
this.authMethodType = AuthMethodType.Password;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -107,6 +107,17 @@ const _getRegistrationError = (state: AuthState) => state.error;
|
||||
*/
|
||||
const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
|
||||
|
||||
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
||||
|
||||
/**
|
||||
* Returns the authentication methods enabled at the backend
|
||||
* @function getAuthenticationMethods
|
||||
* @param {AuthState} state
|
||||
* @param {any} props
|
||||
* @return {any}
|
||||
*/
|
||||
export const getAuthenticationMethods = createSelector(getAuthState, _getAuthenticationMethods);
|
||||
|
||||
/**
|
||||
* Returns the authenticated user
|
||||
* @function getAuthenticatedUser
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { CheckAuthenticationTokenAction } from './auth.actions';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
@@ -43,10 +43,23 @@ export class ServerAuthService extends AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
||||
* Checks if token is present into the request cookie
|
||||
*/
|
||||
public checkAuthenticationToken() {
|
||||
this.store.dispatch(new CheckAuthenticationTokenAction())
|
||||
public checkAuthenticationCookie(): Observable<AuthStatus> {
|
||||
// Determine if the user has an existing auth session on the server
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Accept', 'application/json');
|
||||
if (isNotEmpty(this.req.protocol) && isNotEmpty(this.req.header('host'))) {
|
||||
const referer = this.req.protocol + '://' + this.req.header('host') + this.req.path;
|
||||
// use to allow the rest server to identify the real origin on SSR
|
||||
headers = headers.append('X-Requested-With', referer);
|
||||
}
|
||||
options.headers = headers;
|
||||
options.withCredentials = true;
|
||||
return this.authRequestService.getRequest('status', options).pipe(
|
||||
map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,11 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import {
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormService,
|
||||
DynamicFormValidationService
|
||||
} from '@ng-dynamic-forms/core';
|
||||
|
||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
|
||||
import { Action, StoreConfig, StoreModule } from '@ngrx/store';
|
||||
@@ -17,7 +14,6 @@ import { FormService } from '../shared/form/form.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
||||
|
||||
import {
|
||||
MOCK_RESPONSE_MAP,
|
||||
ResponseMapMock,
|
||||
@@ -47,7 +43,6 @@ import { SubmissionUploadsModel } from './config/models/config-submission-upload
|
||||
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||
|
||||
import { coreEffects } from './core.effects';
|
||||
import { coreReducers, CoreState } from './core.reducers';
|
||||
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
||||
@@ -102,7 +97,6 @@ import { RegistryService } from './registry/registry.service';
|
||||
import { RoleService } from './roles/role.service';
|
||||
|
||||
import { ApiService } from './services/api.service';
|
||||
import { RouteService } from './services/route.service';
|
||||
import { ServerResponseService } from './services/server-response.service';
|
||||
import { NativeWindowFactory, NativeWindowService } from './services/window.service';
|
||||
import { BitstreamFormat } from './shared/bitstream-format.model';
|
||||
@@ -143,6 +137,10 @@ import { PoolTaskDataService } from './tasks/pool-task-data.service';
|
||||
import { TaskResponseParsingService } from './tasks/task-response-parsing.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { storeModuleConfig } from '../app.reducer';
|
||||
import { VersionDataService } from './data/version-data.service';
|
||||
import { VersionHistoryDataService } from './data/version-history-data.service';
|
||||
import { Version } from './shared/version.model';
|
||||
import { VersionHistory } from './shared/version-history.model';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -212,7 +210,6 @@ const PROVIDERS = [
|
||||
BrowseItemsResponseParsingService,
|
||||
BrowseService,
|
||||
ConfigResponseParsingService,
|
||||
RouteService,
|
||||
SubmissionDefinitionsConfigService,
|
||||
SubmissionFormsConfigService,
|
||||
SubmissionRestService,
|
||||
@@ -256,6 +253,8 @@ const PROVIDERS = [
|
||||
RelationshipTypeService,
|
||||
ExternalSourceService,
|
||||
LookupRelationService,
|
||||
VersionDataService,
|
||||
VersionHistoryDataService,
|
||||
LicenseDataService,
|
||||
ItemTypeDataService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
@@ -306,6 +305,8 @@ export const models =
|
||||
ItemType,
|
||||
ExternalSource,
|
||||
ExternalSourceEntry,
|
||||
Version,
|
||||
VersionHistory
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -230,6 +230,8 @@ export class AuthPostRequest extends PostRequest {
|
||||
}
|
||||
|
||||
export class AuthGetRequest extends GetRequest {
|
||||
forceBypassCache = true;
|
||||
|
||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||
super(uuid, href, null, options);
|
||||
}
|
||||
|
44
src/app/core/data/version-data.service.ts
Normal file
44
src/app/core/data/version-data.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DataService } from './data.service';
|
||||
import { Version } from '../shared/version.model';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { VERSION } from '../shared/version.resource-type';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the Version object
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(VERSION)
|
||||
export class VersionDataService extends DataService<Version> {
|
||||
protected linkPath = 'versions';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<Version>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing versions
|
||||
*/
|
||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
}
|
54
src/app/core/data/version-history-data.service.spec.ts
Normal file
54
src/app/core/data/version-history-data.service.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { VersionHistoryDataService } from './version-history-data.service';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { GetRequest } from './request.models';
|
||||
|
||||
const url = 'fake-url';
|
||||
|
||||
describe('VersionHistoryDataService', () => {
|
||||
let service: VersionHistoryDataService;
|
||||
|
||||
let requestService: RequestService;
|
||||
let notificationsService: any;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: any;
|
||||
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
});
|
||||
|
||||
describe('getVersions', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
result = service.getVersions('1');
|
||||
});
|
||||
|
||||
it('should configure a GET request', () => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a VersionHistoryDataService used for testing
|
||||
* @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
|
||||
*/
|
||||
function createService(requestEntry$?) {
|
||||
requestService = getMockRequestService(requestEntry$);
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: jasmine.createSpy('buildList')
|
||||
});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
});
|
||||
halService = new HALEndpointServiceStub(url);
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, null, null);
|
||||
}
|
||||
});
|
81
src/app/core/data/version-history-data.service.ts
Normal file
81
src/app/core/data/version-history-data.service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { DataService } from './data.service';
|
||||
import { VersionHistory } from '../shared/version-history.model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindListOptions, GetRequest } from './request.models';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { Version } from '../shared/version.model';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the VersionHistory object
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(VERSION_HISTORY)
|
||||
export class VersionHistoryDataService extends DataService<VersionHistory> {
|
||||
protected linkPath = 'versionhistories';
|
||||
protected versionsEndpoint = 'versions';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<VersionHistory>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing versions
|
||||
*/
|
||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the versions endpoint for a version history
|
||||
* @param versionHistoryId
|
||||
*/
|
||||
getVersionsEndpoint(versionHistoryId: string): Observable<string> {
|
||||
return this.getBrowseEndpoint().pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.versionsEndpoint, `${href}/${versionHistoryId}`))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a version history's versions using paginated search options
|
||||
* @param versionHistoryId The version history's ID
|
||||
* @param searchOptions The search options to use
|
||||
* @param linksToFollow HAL Links to follow on the Versions
|
||||
*/
|
||||
getVersions(versionHistoryId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Version>>): Observable<RemoteData<PaginatedList<Version>>> {
|
||||
const hrefObs = this.getVersionsEndpoint(versionHistoryId).pipe(
|
||||
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
||||
);
|
||||
hrefObs.pipe(
|
||||
take(1)
|
||||
).subscribe((href) => {
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), href);
|
||||
this.requestService.configure(request);
|
||||
});
|
||||
|
||||
return this.rdbService.buildList<Version>(hrefObs, ...linksToFollow);
|
||||
}
|
||||
}
|
@@ -90,6 +90,14 @@ export class DSpaceRESTv2Service {
|
||||
requestOptions.headers = options.headers;
|
||||
}
|
||||
|
||||
if (options && options.params) {
|
||||
requestOptions.params = options.params;
|
||||
}
|
||||
|
||||
if (options && options.withCredentials) {
|
||||
requestOptions.withCredentials = options.withCredentials;
|
||||
}
|
||||
|
||||
if (!requestOptions.headers.has('Content-Type')) {
|
||||
// Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers
|
||||
requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE);
|
||||
|
@@ -59,7 +59,9 @@ export function parameterSelector(key: string, paramsSelector: (state: CoreState
|
||||
/**
|
||||
* Service to keep track of the current query parameters
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RouteService {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private store: Store<CoreState>) {
|
||||
this.saveRouting();
|
||||
|
@@ -18,6 +18,8 @@ import { Relationship } from './item-relationships/relationship.model';
|
||||
import { RELATIONSHIP } from './item-relationships/relationship.resource-type';
|
||||
import { ITEM } from './item.resource-type';
|
||||
import { ChildHALResource } from './child-hal-resource.model';
|
||||
import { Version } from './version.model';
|
||||
import { VERSION } from './version.resource-type';
|
||||
|
||||
/**
|
||||
* Class representing a DSpace Item
|
||||
@@ -67,6 +69,7 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
||||
bundles: HALLink;
|
||||
owningCollection: HALLink;
|
||||
templateItemOf: HALLink;
|
||||
version: HALLink;
|
||||
self: HALLink;
|
||||
};
|
||||
|
||||
@@ -77,6 +80,13 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
||||
@link(COLLECTION)
|
||||
owningCollection?: Observable<RemoteData<Collection>>;
|
||||
|
||||
/**
|
||||
* The version this item represents in its history
|
||||
* Will be undefined unless the version {@link HALLink} has been resolved.
|
||||
*/
|
||||
@link(VERSION)
|
||||
version?: Observable<RemoteData<Version>>;
|
||||
|
||||
/**
|
||||
* The list of Bundles inside this Item
|
||||
* Will be undefined unless the bundles {@link HALLink} has been resolved.
|
||||
|
39
src/app/core/shared/version-history.model.ts
Normal file
39
src/app/core/shared/version-history.model.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { deserialize, autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { Version } from './version.model';
|
||||
import { VERSION_HISTORY } from './version-history.resource-type';
|
||||
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { VERSION } from './version.resource-type';
|
||||
|
||||
/**
|
||||
* Class representing a DSpace Version History
|
||||
*/
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class VersionHistory extends DSpaceObject {
|
||||
static type = VERSION_HISTORY;
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink;
|
||||
versions: HALLink;
|
||||
};
|
||||
|
||||
/**
|
||||
* The identifier of this Version History
|
||||
*/
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The list of versions within this history
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@link(VERSION, true)
|
||||
versions: Observable<RemoteData<PaginatedList<Version>>>;
|
||||
}
|
9
src/app/core/shared/version-history.resource-type.ts
Normal file
9
src/app/core/shared/version-history.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for VersionHistory
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const VERSION_HISTORY = new ResourceType('versionhistory');
|
76
src/app/core/shared/version.model.ts
Normal file
76
src/app/core/shared/version.model.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { deserialize, autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { Item } from './item.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { VersionHistory } from './version-history.model';
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { VERSION } from './version.resource-type';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||
import { VERSION_HISTORY } from './version-history.resource-type';
|
||||
import { ITEM } from './item.resource-type';
|
||||
import { EPERSON } from '../eperson/models/eperson.resource-type';
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
|
||||
/**
|
||||
* Class representing a DSpace Version
|
||||
*/
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class Version extends DSpaceObject {
|
||||
static type = VERSION;
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink;
|
||||
item: HALLink;
|
||||
versionhistory: HALLink;
|
||||
eperson: HALLink;
|
||||
};
|
||||
|
||||
/**
|
||||
* The identifier of this Version
|
||||
*/
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The version number of the version's history this version represents
|
||||
*/
|
||||
@autoserialize
|
||||
version: number;
|
||||
|
||||
/**
|
||||
* The summary for the changes made in this version
|
||||
*/
|
||||
@autoserialize
|
||||
summary: string;
|
||||
|
||||
/**
|
||||
* The Date this version was created
|
||||
*/
|
||||
@deserialize
|
||||
created: Date;
|
||||
|
||||
/**
|
||||
* The full version history this version is apart of
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@link(VERSION_HISTORY)
|
||||
versionhistory: Observable<RemoteData<VersionHistory>>;
|
||||
|
||||
/**
|
||||
* The item this version represents
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@link(ITEM)
|
||||
item: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The e-person who created this version
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@link(EPERSON)
|
||||
eperson: Observable<RemoteData<EPerson>>;
|
||||
}
|
9
src/app/core/shared/version.resource-type.ts
Normal file
9
src/app/core/shared/version.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for Version
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const VERSION = new ResourceType('version');
|
@@ -1,26 +1,33 @@
|
||||
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" (click)="$event.stopPropagation();">
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
|
||||
(click)="$event.stopPropagation();">
|
||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle class="px-1">{{ 'nav.login' | translate }}</a>
|
||||
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu aria-labelledby="dropdownLogin">
|
||||
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle
|
||||
class="px-1">{{ 'nav.login' | translate }}</a>
|
||||
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
||||
aria-labelledby="dropdownLogin">
|
||||
<ds-log-in
|
||||
[isStandalonePage]="false"></ds-log-in>
|
||||
[isStandalonePage]="false"></ds-log-in>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||
<a id="loginLink" routerLink="/login" routerLinkActive="active" class="px-1" >{{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
|
||||
<a id="loginLink" routerLink="/login" routerLinkActive="active" class="px-1">{{ 'nav.login' | translate }}<span
|
||||
class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="#" id="dropdownUser" (click)="$event.preventDefault()" class="px-1" ngbDropdownToggle><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i></a>
|
||||
<a href="#" id="dropdownUser" (click)="$event.preventDefault()" class="px-1" ngbDropdownToggle><i
|
||||
class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i></a>
|
||||
<div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
|
||||
<ds-user-menu></ds-user-menu>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||
<a id="logoutLink" routerLink="/logout" routerLinkActive="active" class="px-1"><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i><span class="sr-only">(current)</span></a>
|
||||
<a id="logoutLink" routerLink="/logout" routerLinkActive="active" class="px-1"><i
|
||||
class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i><span
|
||||
class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@@ -0,0 +1,47 @@
|
||||
<div *ngVar="(versionsRD$ | async)?.payload as versions">
|
||||
<div *ngVar="(versionRD$ | async)?.payload as itemVersion">
|
||||
<div class="mb-2" *ngIf="versions?.page?.length > 0 || displayWhenEmpty">
|
||||
<h2 *ngIf="displayTitle">{{"item.version.history.head" | translate}}</h2>
|
||||
<ds-pagination *ngIf="versions?.page?.length > 0"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="options"
|
||||
[pageInfoState]="versions"
|
||||
[collectionSize]="versions?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchPage($event)">
|
||||
<table class="table table-striped my-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
||||
<th scope="col">{{"item.version.history.table.item" | translate}}</th>
|
||||
<th scope="col" *ngIf="(hasEpersons$ | async)">{{"item.version.history.table.editor" | translate}}</th>
|
||||
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
|
||||
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
|
||||
<td class="version-row-element-version">{{version?.version}}</td>
|
||||
<td class="version-row-element-item">
|
||||
<span *ngVar="(version?.item | async)?.payload as item">
|
||||
<a *ngIf="item" [routerLink]="['/items', item?.id]">{{item?.handle}}</a>
|
||||
<span *ngIf="version?.id === itemVersion?.id">*</span>
|
||||
</span>
|
||||
</td>
|
||||
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor">
|
||||
<span *ngVar="(version?.eperson | async)?.payload as eperson">
|
||||
<a *ngIf="eperson" [href]="'mailto:' + eperson?.email">{{eperson?.name}}</a>
|
||||
</span>
|
||||
</td>
|
||||
<td class="version-row-element-date">{{version?.created}}</td>
|
||||
<td class="version-row-element-summary">{{version?.summary}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>* {{"item.version.history.selected" | translate}}</div>
|
||||
</ds-pagination>
|
||||
<ds-alert *ngIf="!itemVersion || versions?.page?.length === 0" [content]="'item.version.history.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,121 @@
|
||||
import { ItemVersionsComponent } from './item-versions.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { Version } from '../../../core/shared/version.model';
|
||||
import { VersionHistory } from '../../../core/shared/version-history.model';
|
||||
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../testing/utils';
|
||||
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('ItemVersionsComponent', () => {
|
||||
let component: ItemVersionsComponent;
|
||||
let fixture: ComponentFixture<ItemVersionsComponent>;
|
||||
|
||||
const versionHistory = Object.assign(new VersionHistory(), {
|
||||
id: '1'
|
||||
});
|
||||
const version1 = Object.assign(new Version(), {
|
||||
id: '1',
|
||||
version: 1,
|
||||
created: new Date(2020, 1, 1),
|
||||
summary: 'first version',
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
||||
});
|
||||
const version2 = Object.assign(new Version(), {
|
||||
id: '2',
|
||||
version: 2,
|
||||
summary: 'second version',
|
||||
created: new Date(2020, 1, 2),
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
||||
});
|
||||
const versions = [version1, version2];
|
||||
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
||||
const item1 = Object.assign(new Item(), {
|
||||
uuid: 'item-identifier-1',
|
||||
handle: '123456789/1',
|
||||
version: createSuccessfulRemoteDataObject$(version1)
|
||||
});
|
||||
const item2 = Object.assign(new Item(), {
|
||||
uuid: 'item-identifier-2',
|
||||
handle: '123456789/2',
|
||||
version: createSuccessfulRemoteDataObject$(version2)
|
||||
});
|
||||
const items = [item1, item2];
|
||||
version1.item = createSuccessfulRemoteDataObject$(item1);
|
||||
version2.item = createSuccessfulRemoteDataObject$(item2);
|
||||
const versionHistoryService = jasmine.createSpyObj('versionHistoryService', {
|
||||
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions))
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ItemVersionsComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: VersionHistoryDataService, useValue: versionHistoryService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemVersionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item1;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it(`should display ${versions.length} rows`, () => {
|
||||
const rows = fixture.debugElement.queryAll(By.css('tbody tr'));
|
||||
expect(rows.length).toBe(versions.length);
|
||||
});
|
||||
|
||||
versions.forEach((version: Version, index: number) => {
|
||||
const versionItem = items[index];
|
||||
|
||||
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
|
||||
const id = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
|
||||
expect(id.nativeElement.textContent).toEqual('' + version.version);
|
||||
});
|
||||
|
||||
it(`should display item handle ${versionItem.handle} in the correct column for version ${version.id}`, () => {
|
||||
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-item`));
|
||||
expect(item.nativeElement.textContent).toContain(versionItem.handle);
|
||||
});
|
||||
|
||||
// This version's item is equal to the component's item (the selected item)
|
||||
// Check if the handle contains an asterisk
|
||||
if (item1.uuid === versionItem.uuid) {
|
||||
it('should add an asterisk to the handle of the selected item', () => {
|
||||
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-item`));
|
||||
expect(item.nativeElement.textContent).toContain('*');
|
||||
});
|
||||
}
|
||||
|
||||
it(`should display date ${version.created} in the correct column for version ${version.id}`, () => {
|
||||
const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`));
|
||||
expect(date.nativeElement.textContent).toEqual('' + version.created);
|
||||
});
|
||||
|
||||
it(`should display summary ${version.summary} in the correct column for version ${version.id}`, () => {
|
||||
const summary = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-summary`));
|
||||
expect(summary.nativeElement.textContent).toEqual(version.summary);
|
||||
});
|
||||
});
|
||||
|
||||
describe('switchPage', () => {
|
||||
const page = 5;
|
||||
|
||||
beforeEach(() => {
|
||||
component.switchPage(page);
|
||||
});
|
||||
|
||||
it('should set the option\'s currentPage to the new page', () => {
|
||||
expect(component.options.currentPage).toEqual(page);
|
||||
});
|
||||
});
|
||||
});
|
130
src/app/shared/item/item-versions/item-versions.component.ts
Normal file
130
src/app/shared/item/item-versions/item-versions.component.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { Version } from '../../../core/shared/version.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { VersionHistory } from '../../../core/shared/version-history.model';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||
import { PaginatedSearchOptions } from '../../search/paginated-search-options.model';
|
||||
import { AlertType } from '../../alert/aletr-type';
|
||||
import { followLink } from '../../utils/follow-link-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-versions',
|
||||
templateUrl: './item-versions.component.html'
|
||||
})
|
||||
/**
|
||||
* Component listing all available versions of the history the provided item is a part of
|
||||
*/
|
||||
export class ItemVersionsComponent implements OnInit {
|
||||
/**
|
||||
* The item to display a version history for
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* An option to display the list of versions, even when there aren't any.
|
||||
* Instead of the table, an alert will be displayed, notifying the user there are no other versions present
|
||||
* for the current item.
|
||||
*/
|
||||
@Input() displayWhenEmpty = false;
|
||||
|
||||
/**
|
||||
* Whether or not to display the title
|
||||
*/
|
||||
@Input() displayTitle = true;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
AlertTypeEnum = AlertType;
|
||||
|
||||
/**
|
||||
* The item's version
|
||||
*/
|
||||
versionRD$: Observable<RemoteData<Version>>;
|
||||
|
||||
/**
|
||||
* The item's full version history
|
||||
*/
|
||||
versionHistoryRD$: Observable<RemoteData<VersionHistory>>;
|
||||
|
||||
/**
|
||||
* The version history's list of versions
|
||||
*/
|
||||
versionsRD$: Observable<RemoteData<PaginatedList<Version>>>;
|
||||
|
||||
/**
|
||||
* Verify if the list of versions has at least one e-person to display
|
||||
* Used to hide the "Editor" column when no e-persons are present to display
|
||||
*/
|
||||
hasEpersons$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The amount of versions to display per page
|
||||
*/
|
||||
pageSize = 10;
|
||||
|
||||
/**
|
||||
* The page options to use for fetching the versions
|
||||
* Start at page 1 and always use the set page size
|
||||
*/
|
||||
options = Object.assign(new PaginationComponentOptions(),{
|
||||
id: 'item-versions-options',
|
||||
currentPage: 1,
|
||||
pageSize: this.pageSize
|
||||
});
|
||||
|
||||
/**
|
||||
* The current page being displayed
|
||||
*/
|
||||
currentPage$ = new BehaviorSubject<number>(1);
|
||||
|
||||
constructor(private versionHistoryService: VersionHistoryDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all observables
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.versionRD$ = this.item.version;
|
||||
this.versionHistoryRD$ = this.versionRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
switchMap((version: Version) => version.versionhistory)
|
||||
);
|
||||
const versionHistory$ = this.versionHistoryRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
this.versionsRD$ = observableCombineLatest(versionHistory$, this.currentPage$).pipe(
|
||||
switchMap(([versionHistory, page]: [VersionHistory, number]) =>
|
||||
this.versionHistoryService.getVersions(versionHistory.id,
|
||||
new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}),
|
||||
followLink('item'), followLink('eperson')))
|
||||
);
|
||||
this.hasEpersons$ = this.versionsRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((versions: PaginatedList<Version>) => versions.page.filter((version: Version) => version.eperson !== undefined).length > 0),
|
||||
startWith(false)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current page
|
||||
* @param page
|
||||
*/
|
||||
switchPage(page: number) {
|
||||
this.options.currentPage = page;
|
||||
this.currentPage$.next(page);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
<ds-alert *ngIf="isLatestVersion$ && !(isLatestVersion$ | async)"
|
||||
[content]="('item.version.notice' | translate:{ destination: getItemPage(((latestVersion$ | async)?.item | async)?.payload) })"
|
||||
[dismissible]="false"
|
||||
[type]="AlertTypeEnum.Warning">
|
||||
</ds-alert>
|
@@ -0,0 +1,93 @@
|
||||
import { ItemVersionsNoticeComponent } from './item-versions-notice.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { VersionHistory } from '../../../../core/shared/version-history.model';
|
||||
import { Version } from '../../../../core/shared/version.model';
|
||||
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../testing/utils';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('ItemVersionsNoticeComponent', () => {
|
||||
let component: ItemVersionsNoticeComponent;
|
||||
let fixture: ComponentFixture<ItemVersionsNoticeComponent>;
|
||||
|
||||
const versionHistory = Object.assign(new VersionHistory(), {
|
||||
id: '1'
|
||||
});
|
||||
const firstVersion = Object.assign(new Version(), {
|
||||
id: '1',
|
||||
version: 1,
|
||||
created: new Date(2020, 1, 1),
|
||||
summary: 'first version',
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
||||
});
|
||||
const latestVersion = Object.assign(new Version(), {
|
||||
id: '2',
|
||||
version: 2,
|
||||
summary: 'latest version',
|
||||
created: new Date(2020, 1, 2),
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
||||
});
|
||||
const versions = [latestVersion, firstVersion];
|
||||
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
||||
const firstItem = Object.assign(new Item(), {
|
||||
id: 'first_item_id',
|
||||
uuid: 'first_item_id',
|
||||
handle: '123456789/1',
|
||||
version: createSuccessfulRemoteDataObject$(firstVersion)
|
||||
});
|
||||
const latestItem = Object.assign(new Item(), {
|
||||
id: 'latest_item_id',
|
||||
uuid: 'latest_item_id',
|
||||
handle: '123456789/2',
|
||||
version: createSuccessfulRemoteDataObject$(latestVersion)
|
||||
});
|
||||
firstVersion.item = createSuccessfulRemoteDataObject$(firstItem);
|
||||
latestVersion.item = createSuccessfulRemoteDataObject$(latestItem);
|
||||
const versionHistoryService = jasmine.createSpyObj('versionHistoryService', {
|
||||
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions))
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ItemVersionsNoticeComponent],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: VersionHistoryDataService, useValue: versionHistoryService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
describe('when the item is the latest version', () => {
|
||||
beforeEach(() => {
|
||||
initComponentWithItem(latestItem);
|
||||
});
|
||||
|
||||
it('should not display a notice', () => {
|
||||
const alert = fixture.debugElement.query(By.css('ds-alert'));
|
||||
expect(alert).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not the latest version', () => {
|
||||
beforeEach(() => {
|
||||
initComponentWithItem(firstItem);
|
||||
});
|
||||
|
||||
it('should display a notice', () => {
|
||||
const alert = fixture.debugElement.query(By.css('ds-alert'));
|
||||
expect(alert).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function initComponentWithItem(item: Item) {
|
||||
fixture = TestBed.createComponent(ItemVersionsNoticeComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
}
|
||||
});
|
@@ -0,0 +1,114 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../search/paginated-search-options.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { VersionHistory } from '../../../../core/shared/version-history.model';
|
||||
import { Version } from '../../../../core/shared/version.model';
|
||||
import { hasValue } from '../../../empty.util';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators';
|
||||
import { filter, map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { followLink } from '../../../utils/follow-link-config.model';
|
||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||
import { AlertType } from '../../../alert/aletr-type';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { getItemPageRoute } from '../../../../+item-page/item-page-routing.module';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-versions-notice',
|
||||
templateUrl: './item-versions-notice.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for displaying a warning notice when the item is not the latest version within its version history
|
||||
* The notice contains a link to the latest version's item page
|
||||
*/
|
||||
export class ItemVersionsNoticeComponent implements OnInit {
|
||||
/**
|
||||
* The item to display a version notice for
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The item's version
|
||||
*/
|
||||
versionRD$: Observable<RemoteData<Version>>;
|
||||
|
||||
/**
|
||||
* The item's full version history
|
||||
*/
|
||||
versionHistoryRD$: Observable<RemoteData<VersionHistory>>;
|
||||
|
||||
/**
|
||||
* The latest version of the item's version history
|
||||
*/
|
||||
latestVersion$: Observable<Version>;
|
||||
|
||||
/**
|
||||
* Is the item's version equal to the latest version from the version history?
|
||||
* This will determine whether or not to display a notice linking to the latest version
|
||||
*/
|
||||
isLatestVersion$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Pagination options to fetch a single version on the first page (this is the latest version in the history)
|
||||
*/
|
||||
latestVersionOptions = Object.assign(new PaginationComponentOptions(),{
|
||||
id: 'item-newest-version-options',
|
||||
currentPage: 1,
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
public AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(private versionHistoryService: VersionHistoryDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component's observables
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const latestVersionSearch = new PaginatedSearchOptions({pagination: this.latestVersionOptions});
|
||||
if (hasValue(this.item.version)) {
|
||||
this.versionRD$ = this.item.version;
|
||||
this.versionHistoryRD$ = this.versionRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
switchMap((version: Version) => version.versionhistory)
|
||||
);
|
||||
const versionHistory$ = this.versionHistoryRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
this.latestVersion$ = versionHistory$.pipe(
|
||||
switchMap((versionHistory: VersionHistory) =>
|
||||
this.versionHistoryService.getVersions(versionHistory.id, latestVersionSearch, followLink('item'))),
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
filter((versions) => versions.page.length > 0),
|
||||
map((versions) => versions.page[0])
|
||||
);
|
||||
|
||||
this.isLatestVersion$ = observableCombineLatest(
|
||||
this.versionRD$.pipe(getAllSucceededRemoteData(), getRemoteDataPayload()), this.latestVersion$
|
||||
).pipe(
|
||||
map(([itemVersion, latestVersion]: [Version, Version]) => itemVersion.id === latestVersion.id),
|
||||
startWith(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item page url
|
||||
* @param item The item for which the url is requested
|
||||
*/
|
||||
getItemPage(item: Item): string {
|
||||
if (hasValue(item)) {
|
||||
return getItemPageRoute(item.id);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
<ng-container
|
||||
*ngComponentOutlet="getAuthMethodContent();
|
||||
injector: objectInjector;">
|
||||
</ng-container>
|
||||
|
@@ -0,0 +1,21 @@
|
||||
:host ::ng-deep .card {
|
||||
margin-bottom: $submission-sections-margin-bottom;
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
.section-focus {
|
||||
border-radius: $border-radius;
|
||||
box-shadow: $btn-focus-box-shadow;
|
||||
}
|
||||
|
||||
// TODO to remove the following when upgrading @ng-bootstrap
|
||||
:host ::ng-deep .card:first-of-type {
|
||||
border-bottom: $card-border-width solid $card-border-color !important;
|
||||
border-bottom-left-radius: $card-border-radius !important;
|
||||
border-bottom-right-radius: $card-border-radius !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .card-header button {
|
||||
box-shadow: none !important;
|
||||
width: 100%;
|
||||
}
|
@@ -0,0 +1,108 @@
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { LogInContainerComponent } from './log-in-container.component';
|
||||
import { authReducer } from '../../../core/auth/auth.reducer';
|
||||
import { SharedModule } from '../../shared.module';
|
||||
import { createTestComponent } from '../../testing/utils';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
import { AuthServiceStub } from '../../testing/auth-service-stub';
|
||||
|
||||
describe('LogInContainerComponent', () => {
|
||||
|
||||
let component: LogInContainerComponent;
|
||||
let fixture: ComponentFixture<LogInContainerComponent>;
|
||||
|
||||
const authMethod = new AuthMethod('password');
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
StoreModule.forRoot(authReducer),
|
||||
SharedModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
{provide: AuthService, useClass: AuthServiceStub},
|
||||
LogInContainerComponent
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
let testComp: TestComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
const html = `<ds-log-in-container [authMethod]="authMethod"> </ds-log-in-container>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create LogInContainerComponent', inject([LogInContainerComponent], (app: LogInContainerComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogInContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
spyOn(component, 'getAuthMethodContent').and.callThrough();
|
||||
component.authMethod = authMethod;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
component = null;
|
||||
});
|
||||
|
||||
it('should inject component properly', () => {
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getAuthMethodContent).toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
isStandalonePage = true;
|
||||
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
import { Component, Injector, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { rendersAuthMethodType } from '../methods/log-in.methods-decorator';
|
||||
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
|
||||
/**
|
||||
* This component represents a component container for log-in methods available.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-log-in-container',
|
||||
templateUrl: './log-in-container.component.html',
|
||||
styleUrls: ['./log-in-container.component.scss']
|
||||
})
|
||||
export class LogInContainerComponent implements OnInit {
|
||||
|
||||
@Input() authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* Injector to inject a section component with the @Input parameters
|
||||
* @type {Injector}
|
||||
*/
|
||||
public objectInjector: Injector;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {Injector} injector
|
||||
*/
|
||||
constructor(private injector: Injector) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.objectInjector = Injector.create({
|
||||
providers: [
|
||||
{ provide: 'authMethodProvider', useFactory: () => (this.authMethod), deps: [] },
|
||||
],
|
||||
parent: this.injector
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the correct component based on the AuthMethod's type
|
||||
*/
|
||||
getAuthMethodContent(): string {
|
||||
return rendersAuthMethodType(this.authMethod.authMethodType)
|
||||
}
|
||||
|
||||
}
|
@@ -1,28 +1,13 @@
|
||||
<ds-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-loading>
|
||||
<form *ngIf="!(loading | async) && !(isAuthenticated | async)" class="form-login px-4 py-3" (ngSubmit)="submit()" [formGroup]="form" novalidate>
|
||||
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
||||
<input id="inputEmail"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
class="form-control form-control-lg position-relative"
|
||||
formControlName="email"
|
||||
placeholder="{{'login.form.email' | translate}}"
|
||||
required
|
||||
type="email">
|
||||
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
|
||||
<input id="inputPassword"
|
||||
autocomplete="off"
|
||||
class="form-control form-control-lg position-relative mb-3"
|
||||
placeholder="{{'login.form.password' | translate}}"
|
||||
formControlName="password"
|
||||
required
|
||||
type="password">
|
||||
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" @fadeOut>{{ (error | async) | translate }}</div>
|
||||
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert" @fadeOut>{{ (message | async) | translate }}</div>
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [disabled]="!form.valid">{{"login.form.submit" | translate}}</button>
|
||||
<div *ngIf="!(loading | async) && !(isAuthenticated | async)" class="px-4 py-3 login-container">
|
||||
<ng-container *ngFor="let authMethod of (authMethods | async); let i = index">
|
||||
<div *ngIf="i === 1" class="text-center mt-2">
|
||||
<span class="align-middle">{{"login.form.or-divider" | translate}}</span>
|
||||
</div>
|
||||
<ds-log-in-container [authMethod]="authMethod"></ds-log-in-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>
|
||||
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
@@ -1,13 +1,3 @@
|
||||
.form-login .form-control:focus {
|
||||
z-index: 2;
|
||||
.login-container {
|
||||
max-width: 350px;
|
||||
}
|
||||
.form-login input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.form-login input[type="password"] {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
|
||||
@@ -10,27 +8,27 @@ import { authReducer } from '../../core/auth/auth.reducer';
|
||||
import { EPersonMock } from '../testing/eperson.mock';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { LogInComponent } from './log-in.component';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../testing/auth-service.stub';
|
||||
import { authMethodsMock, AuthServiceStub } from '../testing/auth-service.stub';
|
||||
import { AppState, storeModuleConfig } from '../../app.reducer';
|
||||
|
||||
describe('LogInComponent', () => {
|
||||
|
||||
let component: LogInComponent;
|
||||
let fixture: ComponentFixture<LogInComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
const initialState = {
|
||||
core: {
|
||||
auth: {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: authMethodsMock
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
@@ -43,13 +41,19 @@ describe('LogInComponent', () => {
|
||||
strictActionImmutability: false
|
||||
}
|
||||
}),
|
||||
SharedModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInComponent
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
{provide: AuthService, useClass: AuthServiceStub}
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
provideMockStore({ initialState }),
|
||||
LogInComponent
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
@@ -59,75 +63,58 @@ describe('LogInComponent', () => {
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authState;
|
||||
});
|
||||
describe('', () => {
|
||||
let testComp: TestComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInComponent);
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
const html = `<ds-log-in [isStandalonePage]="isStandalonePage"> </ds-log-in>`;
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
|
||||
// verify the fixture is stable (no pending tasks)
|
||||
fixture.whenStable().then(() => {
|
||||
page.addPageElements();
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
}));
|
||||
afterEach(() => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create a FormGroup comprised of FormControls', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.form instanceof FormGroup).toBe(true);
|
||||
it('should create LogInComponent', inject([LogInComponent], (app: LogInComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
it('should authenticate', () => {
|
||||
fixture.detectChanges();
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogInComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// set FormControl values
|
||||
component.form.controls.email.setValue('user');
|
||||
component.form.controls.password.setValue('password');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
// submit form
|
||||
component.submit();
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
component = null;
|
||||
});
|
||||
|
||||
// verify Store.dispatch() is invoked
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
|
||||
it('should render a log-in container component for each auth method available', () => {
|
||||
const loginContainers = fixture.debugElement.queryAll(By.css('ds-log-in-container'));
|
||||
expect(loginContainers.length).toBe(2);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
isStandalonePage = true;
|
||||
|
||||
constructor(private component: LogInComponent, private fixture: ComponentFixture<LogInComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
public addPageElements() {
|
||||
const emailInputSelector = 'input[formcontrolname=\'email\']';
|
||||
this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement;
|
||||
|
||||
const passwordInputSelector = 'input[formcontrolname=\'password\']';
|
||||
this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement;
|
||||
}
|
||||
}
|
||||
|
@@ -1,26 +1,13 @@
|
||||
import { filter, map, takeWhile } from 'rxjs/operators';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
AuthenticateAction,
|
||||
ResetAuthenticationMessagesAction
|
||||
} from '../../core/auth/auth.actions';
|
||||
import { filter, takeWhile, } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import {
|
||||
getAuthenticationError,
|
||||
getAuthenticationInfo,
|
||||
isAuthenticated,
|
||||
isAuthenticationLoading,
|
||||
} from '../../core/auth/selectors';
|
||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||
import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
|
||||
import { CoreState } from '../../core/core.reducers';
|
||||
|
||||
import { isNotEmpty } from '../empty.util';
|
||||
import { fadeOut } from '../animations/fade';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* /users/sign-in
|
||||
@@ -29,34 +16,21 @@ import { Router } from '@angular/router';
|
||||
@Component({
|
||||
selector: 'ds-log-in',
|
||||
templateUrl: './log-in.component.html',
|
||||
styleUrls: ['./log-in.component.scss'],
|
||||
animations: [fadeOut]
|
||||
styleUrls: ['./log-in.component.scss']
|
||||
})
|
||||
export class LogInComponent implements OnDestroy, OnInit {
|
||||
export class LogInComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The error if authentication fails.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public error: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication error.
|
||||
* A boolean representing if LogInComponent is in a standalone page
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasError = false;
|
||||
@Input() isStandalonePage: boolean;
|
||||
|
||||
/**
|
||||
* The authentication info message.
|
||||
* @type {Observable<string>}
|
||||
* The list of authentication methods available
|
||||
* @type {AuthMethod[]}
|
||||
*/
|
||||
public message: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication message.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasMessage = false;
|
||||
public authMethods: Observable<AuthMethod[]>;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
@@ -70,69 +44,28 @@ export class LogInComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The authentication form.
|
||||
* @type {FormGroup}
|
||||
*/
|
||||
public form: FormGroup;
|
||||
|
||||
/**
|
||||
* Component state.
|
||||
* @type {boolean}
|
||||
*/
|
||||
private alive = true;
|
||||
|
||||
@Input() isStandalonePage: boolean;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthService} authService
|
||||
* @param {FormBuilder} formBuilder
|
||||
* @param {Router} router
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private formBuilder: FormBuilder,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
constructor(private store: Store<CoreState>,
|
||||
private authService: AuthService,) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
|
||||
* @method ngOnInit
|
||||
*/
|
||||
public ngOnInit() {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
ngOnInit(): void {
|
||||
|
||||
// set formGroup
|
||||
this.form = this.formBuilder.group({
|
||||
email: ['', Validators.required],
|
||||
password: ['', Validators.required]
|
||||
});
|
||||
|
||||
// set error
|
||||
this.error = this.store.pipe(select(
|
||||
getAuthenticationError),
|
||||
map((error) => {
|
||||
this.hasError = (isNotEmpty(error));
|
||||
return error;
|
||||
})
|
||||
);
|
||||
|
||||
// set error
|
||||
this.message = this.store.pipe(
|
||||
select(getAuthenticationInfo),
|
||||
map((message) => {
|
||||
this.hasMessage = (isNotEmpty(message));
|
||||
return message;
|
||||
})
|
||||
this.authMethods = this.store.pipe(
|
||||
select(getAuthenticationMethods),
|
||||
);
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// subscribe to success
|
||||
this.store.pipe(
|
||||
select(isAuthenticated),
|
||||
@@ -142,55 +75,11 @@ export class LogInComponent implements OnDestroy, OnInit {
|
||||
this.authService.redirectAfterLoginSuccess(this.isStandalonePage);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook that is called when a directive, pipe or service is destroyed.
|
||||
* @method ngOnDestroy
|
||||
*/
|
||||
public ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
this.alive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset error or message.
|
||||
*/
|
||||
public resetErrorOrMessage() {
|
||||
if (this.hasError || this.hasMessage) {
|
||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||
this.hasError = false;
|
||||
this.hasMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To the registration page.
|
||||
* @method register
|
||||
*/
|
||||
public register() {
|
||||
// TODO enable after registration process is done
|
||||
// this.router.navigate(['/register']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the authentication form.
|
||||
* @method submit
|
||||
*/
|
||||
public submit() {
|
||||
this.resetErrorOrMessage();
|
||||
// get email and password values
|
||||
const email: string = this.form.get('email').value;
|
||||
const password: string = this.form.get('password').value;
|
||||
|
||||
// trim values
|
||||
email.trim();
|
||||
password.trim();
|
||||
|
||||
// dispatch AuthenticationAction
|
||||
this.store.dispatch(new AuthenticateAction(email, password));
|
||||
|
||||
// clear form
|
||||
this.form.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
16
src/app/shared/log-in/methods/log-in.methods-decorator.ts
Normal file
16
src/app/shared/log-in/methods/log-in.methods-decorator.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AuthMethodType } from '../../../core/auth/models/auth.method-type';
|
||||
|
||||
const authMethodsMap = new Map();
|
||||
|
||||
export function renderAuthMethodFor(authMethodType: AuthMethodType) {
|
||||
return function decorator(objectElement: any) {
|
||||
if (!objectElement) {
|
||||
return;
|
||||
}
|
||||
authMethodsMap.set(authMethodType, objectElement);
|
||||
};
|
||||
}
|
||||
|
||||
export function rendersAuthMethodType(authMethodType: AuthMethodType) {
|
||||
return authMethodsMap.get(authMethodType);
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<form (ngSubmit)="submit()"
|
||||
[formGroup]="form" novalidate>
|
||||
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
||||
<input id="inputEmail"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
class="form-control form-control-lg position-relative"
|
||||
formControlName="email"
|
||||
placeholder="{{'login.form.email' | translate}}"
|
||||
required
|
||||
type="email">
|
||||
<label for="inputPassword" class="sr-only">{{"login.form.password" | translate}}</label>
|
||||
<input id="inputPassword"
|
||||
autocomplete="off"
|
||||
class="form-control form-control-lg position-relative mb-3"
|
||||
placeholder="{{'login.form.password' | translate}}"
|
||||
formControlName="password"
|
||||
required
|
||||
type="password">
|
||||
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert"
|
||||
@fadeOut>{{ (error | async) | translate }}</div>
|
||||
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert"
|
||||
@fadeOut>{{ (message | async) | translate }}</div>
|
||||
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit"
|
||||
[disabled]="!form.valid">{{"login.form.submit" | translate}}</button>
|
||||
</form>
|
@@ -0,0 +1,13 @@
|
||||
.form-login .form-control:focus {
|
||||
z-index: 2;
|
||||
}
|
||||
.form-login input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.form-login input[type="password"] {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
@@ -0,0 +1,131 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { LogInPasswordComponent } from './log-in-password.component';
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../../../testing/eperson-mock';
|
||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../testing/auth-service-stub';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
|
||||
describe('LogInPasswordComponent', () => {
|
||||
|
||||
let component: LogInPasswordComponent;
|
||||
let fixture: ComponentFixture<LogInPasswordComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
StoreModule.forRoot(authReducer),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInPasswordComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) }
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authState;
|
||||
});
|
||||
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInPasswordComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
|
||||
// verify the fixture is stable (no pending tasks)
|
||||
fixture.whenStable().then(() => {
|
||||
page.addPageElements();
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
it('should create a FormGroup comprised of FormControls', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.form instanceof FormGroup).toBe(true);
|
||||
});
|
||||
|
||||
it('should authenticate', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// set FormControl values
|
||||
component.form.controls.email.setValue('user');
|
||||
component.form.controls.password.setValue('password');
|
||||
|
||||
// submit form
|
||||
component.submit();
|
||||
|
||||
// verify Store.dispatch() is invoked
|
||||
expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
|
||||
constructor(private component: LogInPasswordComponent, private fixture: ComponentFixture<LogInPasswordComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
public addPageElements() {
|
||||
const emailInputSelector = 'input[formcontrolname=\'email\']';
|
||||
this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement;
|
||||
|
||||
const passwordInputSelector = 'input[formcontrolname=\'password\']';
|
||||
this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement;
|
||||
}
|
||||
}
|
@@ -0,0 +1,144 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../../../core/auth/auth.actions';
|
||||
|
||||
import { getAuthenticationError, getAuthenticationInfo, } from '../../../../core/auth/selectors';
|
||||
import { CoreState } from '../../../../core/core.reducers';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { fadeOut } from '../../../animations/fade';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
|
||||
/**
|
||||
* /users/sign-in
|
||||
* @class LogInPasswordComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-log-in-password',
|
||||
templateUrl: './log-in-password.component.html',
|
||||
styleUrls: ['./log-in-password.component.scss'],
|
||||
animations: [fadeOut]
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Password)
|
||||
export class LogInPasswordComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
* @type {AuthMethod}
|
||||
*/
|
||||
public authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* The error if authentication fails.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public error: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication error.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasError = false;
|
||||
|
||||
/**
|
||||
* The authentication info message.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public message: Observable<string>;
|
||||
|
||||
/**
|
||||
* Has authentication message.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public hasMessage = false;
|
||||
|
||||
/**
|
||||
* The authentication form.
|
||||
* @type {FormGroup}
|
||||
*/
|
||||
public form: FormGroup;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {FormBuilder} formBuilder
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
private formBuilder: FormBuilder,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook that is called after data-bound properties of a directive are initialized.
|
||||
* @method ngOnInit
|
||||
*/
|
||||
public ngOnInit() {
|
||||
|
||||
// set formGroup
|
||||
this.form = this.formBuilder.group({
|
||||
email: ['', Validators.required],
|
||||
password: ['', Validators.required]
|
||||
});
|
||||
|
||||
// set error
|
||||
this.error = this.store.pipe(select(
|
||||
getAuthenticationError),
|
||||
map((error) => {
|
||||
this.hasError = (isNotEmpty(error));
|
||||
return error;
|
||||
})
|
||||
);
|
||||
|
||||
// set error
|
||||
this.message = this.store.pipe(
|
||||
select(getAuthenticationInfo),
|
||||
map((message) => {
|
||||
this.hasMessage = (isNotEmpty(message));
|
||||
return message;
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset error or message.
|
||||
*/
|
||||
public resetErrorOrMessage() {
|
||||
if (this.hasError || this.hasMessage) {
|
||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
||||
this.hasError = false;
|
||||
this.hasMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the authentication form.
|
||||
* @method submit
|
||||
*/
|
||||
public submit() {
|
||||
this.resetErrorOrMessage();
|
||||
// get email and password values
|
||||
const email: string = this.form.get('email').value;
|
||||
const password: string = this.form.get('password').value;
|
||||
|
||||
// trim values
|
||||
email.trim();
|
||||
password.trim();
|
||||
|
||||
// dispatch AuthenticationAction
|
||||
this.store.dispatch(new AuthenticateAction(email, password));
|
||||
|
||||
// clear form
|
||||
this.form.reset();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToShibboleth()" role="button">
|
||||
{{"login.form.shibboleth" | translate}}
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,139 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../../../testing/eperson-mock';
|
||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../testing/auth-service-stub';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInShibbolethComponent } from './log-in-shibboleth.component';
|
||||
import { NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { RouterStub } from '../../../testing/router-stub';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router-stub';
|
||||
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||
|
||||
describe('LogInShibbolethComponent', () => {
|
||||
|
||||
let component: LogInShibbolethComponent;
|
||||
let fixture: ComponentFixture<LogInShibbolethComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
let componentAsAny: any;
|
||||
let setHrefSpy;
|
||||
const shibbolethBaseUrl = 'dspace-rest.test/shibboleth?redirectUrl=';
|
||||
const location = shibbolethBaseUrl + 'http://dspace-angular.test/home';
|
||||
|
||||
const authState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot(authReducer),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInShibbolethComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Shibboleth, location) },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authState;
|
||||
});
|
||||
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInShibbolethComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
componentAsAny = component;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||
|
||||
}));
|
||||
|
||||
it('should set the properly a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/collections/12345';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToShibboleth();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(shibbolethBaseUrl + currentUrl)
|
||||
|
||||
});
|
||||
|
||||
it('should not set a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/home';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToShibboleth();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(shibbolethBaseUrl + currentUrl)
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
|
||||
constructor(private component: LogInShibbolethComponent, private fixture: ComponentFixture<LogInShibbolethComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import { Component, Inject, OnInit, } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
|
||||
import { CoreState } from '../../../../core/core.reducers';
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { isNotNull } from '../../../empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-shibboleth',
|
||||
templateUrl: './log-in-shibboleth.component.html',
|
||||
styleUrls: ['./log-in-shibboleth.component.scss'],
|
||||
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
||||
export class LogInShibbolethComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
* @type {AuthMethod}
|
||||
*/
|
||||
public authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* True if the authentication is loading.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The shibboleth authentication location url.
|
||||
* @type {string}
|
||||
*/
|
||||
public location: string;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {NativeWindowRef} _window
|
||||
* @param {RouteService} route
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
private route: RouteService,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set location
|
||||
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
||||
|
||||
}
|
||||
|
||||
redirectToShibboleth() {
|
||||
let newLocationUrl = this.location;
|
||||
const currentUrl = this._window.nativeWindow.location.href;
|
||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
||||
const match = myRegexp.exec(this.location);
|
||||
const redirectUrl = (match && match[1]) ? match[1] : null;
|
||||
|
||||
// Check whether the current page is different from the redirect url received from rest
|
||||
if (isNotNull(redirectUrl) && redirectUrl !== currentUrl) {
|
||||
// change the redirect url with the current page url
|
||||
const newRedirectUrl = `?redirectUrl=${currentUrl}`;
|
||||
newLocationUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
||||
}
|
||||
|
||||
// redirect to shibboleth authentication url
|
||||
this._window.nativeWindow.location.href = newLocationUrl;
|
||||
}
|
||||
|
||||
}
|
@@ -1 +1 @@
|
||||
@import '../log-in/log-in.component.scss';
|
||||
@import '../log-in/methods/password/log-in-password.component';
|
||||
|
21
src/app/shared/mocks/mock-native-window-ref.ts
Normal file
21
src/app/shared/mocks/mock-native-window-ref.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const MockWindow = {
|
||||
location: {
|
||||
_href: '',
|
||||
set href(url: string) {
|
||||
this._href = url;
|
||||
},
|
||||
get href() {
|
||||
return this._href;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class NativeWindowRefMock {
|
||||
get nativeWindow(): any {
|
||||
return MockWindow;
|
||||
}
|
||||
}
|
||||
|
||||
export function NativeWindowMockFactory() {
|
||||
return new NativeWindowRefMock();
|
||||
}
|
@@ -99,6 +99,13 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
@Input() public hidePagerWhenSinglePage = true;
|
||||
|
||||
/**
|
||||
* Option for disabling updating and reading route parameters on pagination changes
|
||||
* In other words, changing pagination won't add or update the url parameters on the current page, and the url
|
||||
* parameters won't affect the pagination of this component
|
||||
*/
|
||||
@Input() public disableRouteParameterUpdate = false;
|
||||
|
||||
/**
|
||||
* Current page.
|
||||
*/
|
||||
@@ -173,20 +180,35 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
this.checkConfig(this.paginationOptions);
|
||||
this.initializeConfig();
|
||||
// Listen to changes
|
||||
this.subs.push(this.route.queryParams
|
||||
.subscribe((queryParams) => {
|
||||
if (this.isEmptyPaginationParams(queryParams)) {
|
||||
this.initializeConfig(queryParams);
|
||||
if (!this.disableRouteParameterUpdate) {
|
||||
this.subs.push(this.route.queryParams
|
||||
.subscribe((queryParams) => {
|
||||
this.initializeParams(queryParams);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the route and current parameters
|
||||
* This method will fix any invalid or missing parameters
|
||||
* @param params
|
||||
*/
|
||||
private initializeParams(params) {
|
||||
if (this.isEmptyPaginationParams(params)) {
|
||||
this.initializeConfig(params);
|
||||
} else {
|
||||
this.currentQueryParams = params;
|
||||
const fixedProperties = this.validateParams(params);
|
||||
if (isNotEmpty(fixedProperties)) {
|
||||
if (!this.disableRouteParameterUpdate) {
|
||||
this.fixRoute(fixedProperties);
|
||||
} else {
|
||||
this.currentQueryParams = queryParams;
|
||||
const fixedProperties = this.validateParams(queryParams);
|
||||
if (isNotEmpty(fixedProperties)) {
|
||||
this.fixRoute(fixedProperties);
|
||||
} else {
|
||||
this.setFields();
|
||||
}
|
||||
this.initializeParams(fixedProperties);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
this.setFields();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fixRoute(fixedProperties) {
|
||||
@@ -247,7 +269,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* The page being navigated to.
|
||||
*/
|
||||
public doPageChange(page: number) {
|
||||
this.updateRoute({ pageId: this.id, page: page.toString() });
|
||||
this.updateParams(Object.assign({}, this.currentQueryParams, { pageId: this.id, page: page.toString() }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,7 +279,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* The page size being navigated to.
|
||||
*/
|
||||
public doPageSizeChange(pageSize: number) {
|
||||
this.updateRoute({ pageId: this.id, page: 1, pageSize: pageSize });
|
||||
this.updateParams(Object.assign({}, this.currentQueryParams,{ pageId: this.id, page: 1, pageSize: pageSize }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,7 +289,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* The sort direction being navigated to.
|
||||
*/
|
||||
public doSortDirectionChange(sortDirection: SortDirection) {
|
||||
this.updateRoute({ pageId: this.id, page: 1, sortDirection: sortDirection });
|
||||
this.updateParams(Object.assign({}, this.currentQueryParams,{ pageId: this.id, page: 1, sortDirection: sortDirection }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,7 +299,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* The sort field being navigated to.
|
||||
*/
|
||||
public doSortFieldChange(field: string) {
|
||||
this.updateRoute({ pageId: this.id, page: 1, sortField: field });
|
||||
this.updateParams(Object.assign(this.currentQueryParams,{ pageId: this.id, page: 1, sortField: field }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,6 +369,20 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current query params and optionally update the route
|
||||
* @param params
|
||||
*/
|
||||
private updateParams(params: {}) {
|
||||
if (isNotEmpty(difference(params, this.currentQueryParams))) {
|
||||
if (!this.disableRouteParameterUpdate) {
|
||||
this.updateRoute(params);
|
||||
} else {
|
||||
this.initializeParams(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update the route parameters
|
||||
*/
|
||||
|
@@ -42,13 +42,15 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
|
||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
||||
import { VarDirective } from './utils/var.directive';
|
||||
import { LogInComponent } from './log-in/log-in.component';
|
||||
import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
|
||||
import { LogOutComponent } from './log-out/log-out.component';
|
||||
import { FormComponent } from './form/form.component';
|
||||
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component';
|
||||
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
||||
import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
||||
import {
|
||||
DsDynamicFormControlContainerComponent,
|
||||
dsDynamicFormControlMapFn
|
||||
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
||||
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
|
||||
import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
@@ -176,8 +178,14 @@ import { ExternalSourceEntryImportModalComponent } from './form/builder/ds-dynam
|
||||
import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import { ItemVersionsComponent } from './item/item-versions/item-versions.component';
|
||||
import { SortablejsModule } from 'ngx-sortablejs';
|
||||
import { LogInContainerComponent } from './log-in/container/log-in-container.component';
|
||||
import { LogInShibbolethComponent } from './log-in/methods/shibboleth/log-in-shibboleth.component';
|
||||
import { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component';
|
||||
import { LogInComponent } from './log-in/log-in.component';
|
||||
import { MissingTranslationHelper } from './translate/missing-translation.helper';
|
||||
import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -344,7 +352,12 @@ const COMPONENTS = [
|
||||
ExternalSourceEntryImportModalComponent,
|
||||
ImportableListItemControlComponent,
|
||||
ExistingMetadataListElementComponent,
|
||||
LogInShibbolethComponent,
|
||||
LogInPasswordComponent,
|
||||
LogInContainerComponent,
|
||||
ItemVersionsComponent,
|
||||
PublicationSearchResultListElementComponent,
|
||||
ItemVersionsNoticeComponent
|
||||
];
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
@@ -408,6 +421,10 @@ const ENTRY_COMPONENTS = [
|
||||
DsDynamicLookupRelationSelectionTabComponent,
|
||||
DsDynamicLookupRelationExternalSourceTabComponent,
|
||||
ExternalSourceEntryImportModalComponent,
|
||||
LogInPasswordComponent,
|
||||
LogInShibbolethComponent,
|
||||
ItemVersionsComponent,
|
||||
ItemVersionsNoticeComponent
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
|
@@ -23,7 +23,7 @@ export class AuthRequestServiceStub {
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
} else {
|
||||
} else if (isNotEmpty(options)) {
|
||||
const token = (options.headers as any).lazyUpdate[1].value;
|
||||
if (this.validateToken(token)) {
|
||||
authStatusStub.authenticated = true;
|
||||
@@ -32,6 +32,8 @@ export class AuthRequestServiceStub {
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
} else {
|
||||
authStatusStub.authenticated = false;
|
||||
}
|
||||
return observableOf(authStatusStub);
|
||||
}
|
||||
@@ -43,7 +45,7 @@ export class AuthRequestServiceStub {
|
||||
authStatusStub.authenticated = false;
|
||||
break;
|
||||
case 'status':
|
||||
const token = (options.headers as any).lazyUpdate[1].value;
|
||||
const token = ((options.headers as any).lazyUpdate[1]) ? (options.headers as any).lazyUpdate[1].value : null;
|
||||
if (this.validateToken(token)) {
|
||||
authStatusStub.authenticated = true;
|
||||
authStatusStub.token = this.mockTokenInfo;
|
||||
|
@@ -4,6 +4,12 @@ import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||
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';
|
||||
|
||||
export const authMethodsMock = [
|
||||
new AuthMethod('password'),
|
||||
new AuthMethod('shibboleth', 'dspace.test/shibboleth')
|
||||
];
|
||||
|
||||
export class AuthServiceStub {
|
||||
|
||||
@@ -106,4 +112,12 @@ export class AuthServiceStub {
|
||||
isAuthenticated() {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
checkAuthenticationCookie() {
|
||||
return;
|
||||
}
|
||||
|
||||
retrieveAuthMethodsFromAuthStatus(status: AuthStatus) {
|
||||
return observableOf(authMethodsMock);
|
||||
}
|
||||
}
|
||||
|
@@ -1005,6 +1005,12 @@
|
||||
|
||||
"item.edit.tabs.status.title": "Item Edit - Status",
|
||||
|
||||
"item.edit.tabs.versionhistory.head": "Version History",
|
||||
|
||||
"item.edit.tabs.versionhistory.title": "Item Edit - Version History",
|
||||
|
||||
"item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.",
|
||||
|
||||
"item.edit.tabs.view.head": "View Item",
|
||||
|
||||
"item.edit.tabs.view.title": "Item Edit - View",
|
||||
@@ -1084,6 +1090,29 @@
|
||||
"item.select.table.title": "Title",
|
||||
|
||||
|
||||
"item.version.history.empty": "There are no other versions for this item yet.",
|
||||
|
||||
"item.version.history.head": "Version History",
|
||||
|
||||
"item.version.history.return": "Return",
|
||||
|
||||
"item.version.history.selected": "Selected version",
|
||||
|
||||
"item.version.history.table.version": "Version",
|
||||
|
||||
"item.version.history.table.item": "Item",
|
||||
|
||||
"item.version.history.table.editor": "Editor",
|
||||
|
||||
"item.version.history.table.date": "Date",
|
||||
|
||||
"item.version.history.table.summary": "Summary",
|
||||
|
||||
|
||||
|
||||
"item.version.notice": "This is not the latest version of this item. The latest version can be found <a href='{{destination}}'>here</a>.",
|
||||
|
||||
|
||||
|
||||
"journal.listelement.badge": "Journal",
|
||||
|
||||
@@ -1175,8 +1204,12 @@
|
||||
|
||||
"login.form.new-user": "New user? Click here to register.",
|
||||
|
||||
"login.form.or-divider": "or",
|
||||
|
||||
"login.form.password": "Password",
|
||||
|
||||
"login.form.shibboleth": "Log in with Shibboleth",
|
||||
|
||||
"login.form.submit": "Log in",
|
||||
|
||||
"login.title": "Login",
|
||||
|
@@ -918,7 +918,7 @@
|
||||
"item.edit.move.processing": "Movendo...",
|
||||
|
||||
// "item.edit.move.search.placeholder": "Enter a search query to look for collections",
|
||||
"item.edit.move.search.placeholder": "nsira uma consulta para procurar coleções",
|
||||
"item.edit.move.search.placeholder": "Insira uma consulta para procurar coleções",
|
||||
|
||||
// "item.edit.move.success": "The item has been moved successfully",
|
||||
"item.edit.move.success": "O item foi movido com sucesso",
|
||||
|
10
src/config/auth-config.interfaces.ts
Normal file
10
src/config/auth-config.interfaces.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Config } from './config.interface';
|
||||
|
||||
export interface AuthTarget {
|
||||
host: string;
|
||||
page: string;
|
||||
}
|
||||
|
||||
export interface AuthConfig extends Config {
|
||||
target: AuthTarget;
|
||||
}
|
@@ -10,12 +10,14 @@ import { BrowseByConfig } from './browse-by-config.interface';
|
||||
import { ItemPageConfig } from './item-page-config.interface';
|
||||
import { CollectionPageConfig } from './collection-page-config.interface';
|
||||
import { Theme } from './theme.inferface';
|
||||
import {AuthConfig} from './auth-config.interfaces';
|
||||
|
||||
export interface GlobalConfig extends Config {
|
||||
ui: ServerConfig;
|
||||
rest: ServerConfig;
|
||||
production: boolean;
|
||||
cache: CacheConfig;
|
||||
auth: AuthConfig;
|
||||
form: FormConfig;
|
||||
notifications: INotificationBoardOptions;
|
||||
submission: SubmissionConfig;
|
||||
|
Reference in New Issue
Block a user