mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +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 { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.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
|
* 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,
|
ItemMetadataComponent,
|
||||||
ItemRelationshipsComponent,
|
ItemRelationshipsComponent,
|
||||||
ItemBitstreamsComponent,
|
ItemBitstreamsComponent,
|
||||||
|
ItemVersionHistoryComponent,
|
||||||
EditInPlaceFieldComponent,
|
EditInPlaceFieldComponent,
|
||||||
EditRelationshipComponent,
|
EditRelationshipComponent,
|
||||||
EditRelationshipListComponent,
|
EditRelationshipListComponent,
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { ItemPageResolver } from '../item-page.resolver';
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { EditItemPageComponent } from './edit-item-page.component';
|
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 { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
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_WITHDRAW_PATH = 'withdraw';
|
||||||
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||||
@@ -75,6 +75,11 @@ export const ITEM_EDIT_MOVE_PATH = 'move';
|
|||||||
/* TODO - change when curate page exists */
|
/* TODO - change when curate page exists */
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
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="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="itemRD?.payload as item">
|
<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-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||||
<div class="simple-view-link my-3">
|
<div class="simple-view-link my-3">
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
</table>
|
</table>
|
||||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
<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-page-collections [item]="item"></ds-item-page-collections>
|
||||||
|
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
|
@@ -59,7 +59,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
|
|||||||
MetadataRepresentationListComponent,
|
MetadataRepresentationListComponent,
|
||||||
RelatedEntitiesSearchComponent,
|
RelatedEntitiesSearchComponent,
|
||||||
TabbedRelatedEntitiesSearchComponent,
|
TabbedRelatedEntitiesSearchComponent,
|
||||||
AbstractIncrementalListComponent
|
AbstractIncrementalListComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ItemComponent,
|
ItemComponent,
|
||||||
|
@@ -28,6 +28,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
followLink('owningCollection'),
|
followLink('owningCollection'),
|
||||||
followLink('bundles'),
|
followLink('bundles'),
|
||||||
followLink('relationships'),
|
followLink('relationships'),
|
||||||
|
followLink('version', undefined, true, followLink('versionhistory')),
|
||||||
).pipe(
|
).pipe(
|
||||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
);
|
);
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="itemRD?.payload as item">
|
<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-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<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: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ 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: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
||||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
{ 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: 'workspaceitems',
|
||||||
{ path: PROFILE_MODULE_PATH, loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard] },
|
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 },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@@ -9,6 +9,7 @@ import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest }
|
|||||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
@@ -16,7 +17,8 @@ export class AuthRequestService {
|
|||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
constructor(protected halService: HALEndpointService,
|
constructor(protected halService: HALEndpointService,
|
||||||
protected requestService: RequestService) {
|
protected requestService: RequestService,
|
||||||
|
private http: HttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||||
@@ -36,7 +38,7 @@ export class AuthRequestService {
|
|||||||
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
|
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(
|
return this.halService.getEndpoint(this.linkName).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||||
|
@@ -5,6 +5,8 @@ import { type } from '../../shared/ngrx/type';
|
|||||||
// import models
|
// import models
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.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 = {
|
export const AuthActionTypes = {
|
||||||
AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
|
AUTHENTICATE: type('dspace/auth/AUTHENTICATE'),
|
||||||
@@ -14,12 +16,16 @@ export const AuthActionTypes = {
|
|||||||
AUTHENTICATED_ERROR: type('dspace/auth/AUTHENTICATED_ERROR'),
|
AUTHENTICATED_ERROR: type('dspace/auth/AUTHENTICATED_ERROR'),
|
||||||
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
|
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
|
||||||
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
|
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
|
||||||
CHECK_AUTHENTICATION_TOKEN_ERROR: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_ERROR'),
|
CHECK_AUTHENTICATION_TOKEN_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_TOKEN_EXPIRED: type('dspace/auth/REDIRECT_TOKEN_EXPIRED'),
|
||||||
REDIRECT_AUTHENTICATION_REQUIRED: type('dspace/auth/REDIRECT_AUTHENTICATION_REQUIRED'),
|
REDIRECT_AUTHENTICATION_REQUIRED: type('dspace/auth/REDIRECT_AUTHENTICATION_REQUIRED'),
|
||||||
REFRESH_TOKEN: type('dspace/auth/REFRESH_TOKEN'),
|
REFRESH_TOKEN: type('dspace/auth/REFRESH_TOKEN'),
|
||||||
REFRESH_TOKEN_SUCCESS: type('dspace/auth/REFRESH_TOKEN_SUCCESS'),
|
REFRESH_TOKEN_SUCCESS: type('dspace/auth/REFRESH_TOKEN_SUCCESS'),
|
||||||
REFRESH_TOKEN_ERROR: type('dspace/auth/REFRESH_TOKEN_ERROR'),
|
REFRESH_TOKEN_ERROR: type('dspace/auth/REFRESH_TOKEN_ERROR'),
|
||||||
|
RETRIEVE_TOKEN: type('dspace/auth/RETRIEVE_TOKEN'),
|
||||||
ADD_MESSAGE: type('dspace/auth/ADD_MESSAGE'),
|
ADD_MESSAGE: type('dspace/auth/ADD_MESSAGE'),
|
||||||
RESET_MESSAGES: type('dspace/auth/RESET_MESSAGES'),
|
RESET_MESSAGES: type('dspace/auth/RESET_MESSAGES'),
|
||||||
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
||||||
@@ -95,7 +101,7 @@ export class AuthenticatedErrorAction implements Action {
|
|||||||
payload: Error;
|
payload: Error;
|
||||||
|
|
||||||
constructor(payload: Error) {
|
constructor(payload: Error) {
|
||||||
this.payload = payload ;
|
this.payload = payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +115,7 @@ export class AuthenticationErrorAction implements Action {
|
|||||||
payload: Error;
|
payload: Error;
|
||||||
|
|
||||||
constructor(payload: Error) {
|
constructor(payload: Error) {
|
||||||
this.payload = payload ;
|
this.payload = payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +144,11 @@ export class CheckAuthenticationTokenAction implements Action {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check Authentication Token Error.
|
* Check Authentication Token Error.
|
||||||
* @class CheckAuthenticationTokenErrorAction
|
* @class CheckAuthenticationTokenCookieAction
|
||||||
* @implements {Action}
|
* @implements {Action}
|
||||||
*/
|
*/
|
||||||
export class CheckAuthenticationTokenErrorAction implements Action {
|
export class CheckAuthenticationTokenCookieAction implements Action {
|
||||||
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_ERROR;
|
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,7 +158,9 @@ export class CheckAuthenticationTokenErrorAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export class LogOutAction implements Action {
|
export class LogOutAction implements Action {
|
||||||
public type: string = AuthActionTypes.LOG_OUT;
|
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;
|
payload: Error;
|
||||||
|
|
||||||
constructor(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 {
|
export class LogOutSuccessAction implements Action {
|
||||||
public type: string = AuthActionTypes.LOG_OUT_SUCCESS;
|
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;
|
payload: string;
|
||||||
|
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
this.payload = message ;
|
this.payload = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +213,7 @@ export class RedirectWhenTokenExpiredAction implements Action {
|
|||||||
payload: string;
|
payload: string;
|
||||||
|
|
||||||
constructor(message: 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;
|
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.
|
* Sign up.
|
||||||
* @class RegistrationAction
|
* @class RegistrationAction
|
||||||
@@ -268,7 +287,7 @@ export class RegistrationErrorAction implements Action {
|
|||||||
payload: Error;
|
payload: Error;
|
||||||
|
|
||||||
constructor(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;
|
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.
|
* Change the redirect url.
|
||||||
* @class SetRedirectUrlAction
|
* @class SetRedirectUrlAction
|
||||||
@@ -319,7 +377,7 @@ export class SetRedirectUrlAction implements Action {
|
|||||||
payload: string;
|
payload: string;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
this.payload = url ;
|
this.payload = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,13 +436,21 @@ export type AuthActions
|
|||||||
| AuthenticationErrorAction
|
| AuthenticationErrorAction
|
||||||
| AuthenticationSuccessAction
|
| AuthenticationSuccessAction
|
||||||
| CheckAuthenticationTokenAction
|
| CheckAuthenticationTokenAction
|
||||||
| CheckAuthenticationTokenErrorAction
|
| CheckAuthenticationTokenCookieAction
|
||||||
| RedirectWhenAuthenticationIsRequiredAction
|
| RedirectWhenAuthenticationIsRequiredAction
|
||||||
| RedirectWhenTokenExpiredAction
|
| RedirectWhenTokenExpiredAction
|
||||||
| RegistrationAction
|
| RegistrationAction
|
||||||
| RegistrationErrorAction
|
| RegistrationErrorAction
|
||||||
| RegistrationSuccessAction
|
| RegistrationSuccessAction
|
||||||
| AddAuthenticationMessageAction
|
| AddAuthenticationMessageAction
|
||||||
|
| RefreshTokenAction
|
||||||
|
| RefreshTokenErrorAction
|
||||||
|
| RefreshTokenSuccessAction
|
||||||
|
| ResetAuthenticationMessagesAction
|
||||||
|
| RetrieveAuthMethodsAction
|
||||||
|
| RetrieveAuthMethodsSuccessAction
|
||||||
|
| RetrieveAuthMethodsErrorAction
|
||||||
|
| RetrieveTokenAction
|
||||||
| ResetAuthenticationMessagesAction
|
| ResetAuthenticationMessagesAction
|
||||||
| RetrieveAuthenticatedEpersonAction
|
| RetrieveAuthenticatedEpersonAction
|
||||||
| RetrieveAuthenticatedEpersonErrorAction
|
| RetrieveAuthenticatedEpersonErrorAction
|
||||||
|
@@ -14,20 +14,26 @@ import {
|
|||||||
AuthenticatedSuccessAction,
|
AuthenticatedSuccessAction,
|
||||||
AuthenticationErrorAction,
|
AuthenticationErrorAction,
|
||||||
AuthenticationSuccessAction,
|
AuthenticationSuccessAction,
|
||||||
CheckAuthenticationTokenErrorAction,
|
CheckAuthenticationTokenCookieAction,
|
||||||
LogOutErrorAction,
|
LogOutErrorAction,
|
||||||
LogOutSuccessAction,
|
LogOutSuccessAction,
|
||||||
RefreshTokenErrorAction,
|
RefreshTokenErrorAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
RetrieveAuthenticatedEpersonAction,
|
RetrieveAuthenticatedEpersonAction,
|
||||||
RetrieveAuthenticatedEpersonErrorAction,
|
RetrieveAuthenticatedEpersonErrorAction,
|
||||||
RetrieveAuthenticatedEpersonSuccessAction
|
RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
|
RetrieveAuthMethodsAction,
|
||||||
|
RetrieveAuthMethodsErrorAction,
|
||||||
|
RetrieveAuthMethodsSuccessAction,
|
||||||
|
RetrieveTokenAction
|
||||||
} from './auth.actions';
|
} 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 { AuthService } from './auth.service';
|
||||||
import { AuthState } from './auth.reducer';
|
import { AuthState } from './auth.reducer';
|
||||||
|
|
||||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||||
|
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||||
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
|
|
||||||
describe('AuthEffects', () => {
|
describe('AuthEffects', () => {
|
||||||
let authEffects: AuthEffects;
|
let authEffects: AuthEffects;
|
||||||
@@ -169,13 +175,56 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token } });
|
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);
|
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('retrieveAuthenticatedEperson$', () => {
|
||||||
|
|
||||||
describe('when request is successful', () => {
|
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('logOut$', () => {
|
||||||
|
|
||||||
describe('when refresh token succeeded', () => {
|
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 { Injectable } from '@angular/core';
|
||||||
|
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
@@ -9,6 +9,14 @@ import { Action, select, Store } from '@ngrx/store';
|
|||||||
|
|
||||||
// import services
|
// import services
|
||||||
import { AuthService } from './auth.service';
|
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 actions
|
||||||
import {
|
import {
|
||||||
AuthActionTypes,
|
AuthActionTypes,
|
||||||
@@ -18,7 +26,7 @@ import {
|
|||||||
AuthenticatedSuccessAction,
|
AuthenticatedSuccessAction,
|
||||||
AuthenticationErrorAction,
|
AuthenticationErrorAction,
|
||||||
AuthenticationSuccessAction,
|
AuthenticationSuccessAction,
|
||||||
CheckAuthenticationTokenErrorAction,
|
CheckAuthenticationTokenCookieAction,
|
||||||
LogOutErrorAction,
|
LogOutErrorAction,
|
||||||
LogOutSuccessAction,
|
LogOutSuccessAction,
|
||||||
RefreshTokenAction,
|
RefreshTokenAction,
|
||||||
@@ -29,14 +37,12 @@ import {
|
|||||||
RegistrationSuccessAction,
|
RegistrationSuccessAction,
|
||||||
RetrieveAuthenticatedEpersonAction,
|
RetrieveAuthenticatedEpersonAction,
|
||||||
RetrieveAuthenticatedEpersonErrorAction,
|
RetrieveAuthenticatedEpersonErrorAction,
|
||||||
RetrieveAuthenticatedEpersonSuccessAction
|
RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
|
RetrieveAuthMethodsAction,
|
||||||
|
RetrieveAuthMethodsErrorAction,
|
||||||
|
RetrieveAuthMethodsSuccessAction,
|
||||||
|
RetrieveTokenAction
|
||||||
} from './auth.actions';
|
} 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()
|
@Injectable()
|
||||||
export class AuthEffects {
|
export class AuthEffects {
|
||||||
@@ -102,7 +108,24 @@ export class AuthEffects {
|
|||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
return this.authService.hasValidAuthenticationToken().pipe(
|
return this.authService.hasValidAuthenticationToken().pipe(
|
||||||
map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
|
map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
|
||||||
catchError((error) => observableOf(new CheckAuthenticationTokenErrorAction()))
|
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)))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -119,6 +142,18 @@ export class AuthEffects {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@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()
|
@Effect()
|
||||||
public refreshToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN),
|
public refreshToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN),
|
||||||
switchMap((action: RefreshTokenAction) => {
|
switchMap((action: RefreshTokenAction) => {
|
||||||
@@ -188,6 +223,19 @@ export class AuthEffects {
|
|||||||
tap(() => this.authService.redirectToLoginWhenTokenExpired())
|
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
|
* @constructor
|
||||||
* @param {Actions} actions$
|
* @param {Actions} actions$
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
HttpErrorResponse,
|
HttpErrorResponse,
|
||||||
HttpEvent,
|
HttpEvent,
|
||||||
HttpHandler,
|
HttpHandler,
|
||||||
|
HttpHeaders,
|
||||||
HttpInterceptor,
|
HttpInterceptor,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
@@ -17,10 +18,12 @@ import { AppState } from '../../app.reducer';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.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 { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthMethod } from './models/auth.method';
|
||||||
|
import { AuthMethodType } from './models/auth.method-type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
@@ -30,17 +33,33 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
// we're creating a refresh token request list
|
// we're creating a refresh token request list
|
||||||
protected refreshTokenRequestUrls = [];
|
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 {
|
private isUnauthorized(response: HttpResponseBase): boolean {
|
||||||
// invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons
|
// invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||||
return response.status === 401;
|
return response.status === 401;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if response status code is 200 or 204
|
||||||
|
*
|
||||||
|
* @param response
|
||||||
|
*/
|
||||||
private isSuccess(response: HttpResponseBase): boolean {
|
private isSuccess(response: HttpResponseBase): boolean {
|
||||||
return (response.status === 200 || response.status === 204);
|
return (response.status === 200 || response.status === 204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if http request is to authn endpoint
|
||||||
|
*
|
||||||
|
* @param http
|
||||||
|
*/
|
||||||
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
|
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
return http && http.url
|
return http && http.url
|
||||||
&& (http.url.endsWith('/authn/login')
|
&& (http.url.endsWith('/authn/login')
|
||||||
@@ -48,18 +67,131 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
|| http.url.endsWith('/authn/status'));
|
|| http.url.endsWith('/authn/status'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if response is from a login request
|
||||||
|
*
|
||||||
|
* @param http
|
||||||
|
*/
|
||||||
private isLoginResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
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 {
|
private isLogoutResponse(http: HttpRequest<any> | HttpResponseBase): boolean {
|
||||||
return http.url && http.url.endsWith('/authn/logout');
|
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();
|
const authStatus = new AuthStatus();
|
||||||
|
// let authMethods: AuthMethodModel[];
|
||||||
|
if (httpHeaders) {
|
||||||
|
authStatus.authMethods = this.parseAuthMethodsFromHeaders(httpHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
authStatus.id = null;
|
authStatus.id = null;
|
||||||
|
|
||||||
authStatus.okay = true;
|
authStatus.okay = true;
|
||||||
|
// authStatus.authMethods = authMethods;
|
||||||
|
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
authStatus.authenticated = true;
|
authStatus.authenticated = true;
|
||||||
authStatus.token = new AuthTokenInfo(accessToken);
|
authStatus.token = new AuthTokenInfo(accessToken);
|
||||||
@@ -70,12 +202,18 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
return authStatus;
|
return authStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept method
|
||||||
|
* @param req
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
|
||||||
const authService = this.inj.get(AuthService);
|
const authService = this.inj.get(AuthService);
|
||||||
|
|
||||||
const token = authService.getToken();
|
const token: AuthTokenInfo = authService.getToken();
|
||||||
let newReq;
|
let newReq: HttpRequest<any>;
|
||||||
|
let authorization: string;
|
||||||
|
|
||||||
if (authService.isTokenExpired()) {
|
if (authService.isTokenExpired()) {
|
||||||
authService.setRedirectUrl(this.router.url);
|
authService.setRedirectUrl(this.router.url);
|
||||||
@@ -96,30 +234,41 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Get the auth header from the service.
|
// Get the auth header from the service.
|
||||||
const Authorization = authService.buildAuthHeader(token);
|
authorization = authService.buildAuthHeader(token);
|
||||||
// Clone the request to add the new header.
|
// 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 {
|
} else {
|
||||||
newReq = req;
|
newReq = req.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass on the new request instead of the original request.
|
// Pass on the new request instead of the original request.
|
||||||
return next.handle(newReq).pipe(
|
return next.handle(newReq).pipe(
|
||||||
|
// tap((response) => console.log('next.handle: ', response)),
|
||||||
map((response) => {
|
map((response) => {
|
||||||
// Intercept a Login/Logout 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
|
// It's a success Login/Logout response
|
||||||
let authRes: HttpResponse<any>;
|
let authRes: HttpResponse<any>;
|
||||||
if (this.isLoginResponse(response)) {
|
if (this.isLoginResponse(response)) {
|
||||||
// login successfully
|
// login successfully
|
||||||
const newToken = response.headers.get('authorization');
|
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
|
// clean eventually refresh Requests list
|
||||||
this.refreshTokenRequestUrls = [];
|
this.refreshTokenRequestUrls = [];
|
||||||
|
} else if (this.isStatusResponse(response)) {
|
||||||
|
authRes = response.clone({
|
||||||
|
body: Object.assign(response.body, {
|
||||||
|
authMethods: this.parseAuthMethodsFromHeaders(response.headers)
|
||||||
|
})
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// logout successfully
|
// logout successfully
|
||||||
authRes = response.clone({body: this.makeAuthStatusObject(false)});
|
authRes = response.clone({
|
||||||
|
body: this.makeAuthStatusObject(false)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return authRes;
|
return authRes;
|
||||||
} else {
|
} else {
|
||||||
@@ -129,13 +278,15 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
catchError((error, caught) => {
|
catchError((error, caught) => {
|
||||||
// Intercept an error response
|
// Intercept an error response
|
||||||
if (error instanceof HttpErrorResponse) {
|
if (error instanceof HttpErrorResponse) {
|
||||||
|
|
||||||
// Checks if is a response from a request to an authentication endpoint
|
// Checks if is a response from a request to an authentication endpoint
|
||||||
if (this.isAuthRequest(error)) {
|
if (this.isAuthRequest(error)) {
|
||||||
// clean eventually refresh Requests list
|
// clean eventually refresh Requests list
|
||||||
this.refreshTokenRequestUrls = [];
|
this.refreshTokenRequestUrls = [];
|
||||||
|
|
||||||
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
|
// Create a new HttpResponse and return it, so it can be handle properly by AuthService.
|
||||||
const authResponse = new HttpResponse({
|
const authResponse = new HttpResponse({
|
||||||
body: this.makeAuthStatusObject(false, null, error.error),
|
body: this.makeAuthStatusObject(false, null, error.error, error.headers),
|
||||||
headers: error.headers,
|
headers: error.headers,
|
||||||
status: error.status,
|
status: error.status,
|
||||||
statusText: error.statusText,
|
statusText: error.statusText,
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
AuthenticationErrorAction,
|
AuthenticationErrorAction,
|
||||||
AuthenticationSuccessAction,
|
AuthenticationSuccessAction,
|
||||||
CheckAuthenticationTokenAction,
|
CheckAuthenticationTokenAction,
|
||||||
CheckAuthenticationTokenErrorAction,
|
CheckAuthenticationTokenCookieAction,
|
||||||
LogOutAction,
|
LogOutAction,
|
||||||
LogOutErrorAction,
|
LogOutErrorAction,
|
||||||
LogOutSuccessAction,
|
LogOutSuccessAction,
|
||||||
@@ -17,11 +17,19 @@ import {
|
|||||||
RefreshTokenAction,
|
RefreshTokenAction,
|
||||||
RefreshTokenErrorAction,
|
RefreshTokenErrorAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
ResetAuthenticationMessagesAction, RetrieveAuthenticatedEpersonErrorAction, RetrieveAuthenticatedEpersonSuccessAction,
|
ResetAuthenticationMessagesAction,
|
||||||
|
RetrieveAuthenticatedEpersonErrorAction,
|
||||||
|
RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
|
RetrieveAuthMethodsAction,
|
||||||
|
RetrieveAuthMethodsErrorAction,
|
||||||
|
RetrieveAuthMethodsSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
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', () => {
|
describe('authReducer', () => {
|
||||||
|
|
||||||
@@ -157,18 +165,18 @@ describe('authReducer', () => {
|
|||||||
expect(newState).toEqual(state);
|
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 = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenErrorAction();
|
const action = new CheckAuthenticationTokenCookieAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
loading: false,
|
loading: true,
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -451,4 +459,63 @@ describe('authReducer', () => {
|
|||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
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,
|
LogOutErrorAction,
|
||||||
RedirectWhenAuthenticationIsRequiredAction,
|
RedirectWhenAuthenticationIsRequiredAction,
|
||||||
RedirectWhenTokenExpiredAction,
|
RedirectWhenTokenExpiredAction,
|
||||||
RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
|
RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
|
RetrieveAuthMethodsSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
// import models
|
// import models
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.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.
|
* The auth state.
|
||||||
@@ -47,6 +51,10 @@ export interface AuthState {
|
|||||||
|
|
||||||
// the authenticated user
|
// the authenticated user
|
||||||
user?: EPerson;
|
user?: EPerson;
|
||||||
|
|
||||||
|
// all authentication Methods enabled at the backend
|
||||||
|
authMethods?: AuthMethod[];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +64,7 @@ const initialState: AuthState = {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
authMethods: []
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,6 +84,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATED:
|
case AuthActionTypes.AUTHENTICATED:
|
||||||
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||||
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: true
|
loading: true
|
||||||
});
|
});
|
||||||
@@ -113,21 +124,10 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATED:
|
|
||||||
case AuthActionTypes.AUTHENTICATE_SUCCESS:
|
case AuthActionTypes.AUTHENTICATE_SUCCESS:
|
||||||
case AuthActionTypes.LOG_OUT:
|
case AuthActionTypes.LOG_OUT:
|
||||||
return state;
|
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:
|
case AuthActionTypes.LOG_OUT_ERROR:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@@ -192,6 +192,24 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
info: undefined,
|
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:
|
case AuthActionTypes.SET_REDIRECT_URL:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
redirectUrl: (action as SetRedirectUrlAction).payload,
|
redirectUrl: (action as SetRedirectUrlAction).payload,
|
||||||
|
@@ -28,6 +28,8 @@ import { Observable } from 'rxjs/internal/Observable';
|
|||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
||||||
|
import { AuthMethod } from './models/auth.method';
|
||||||
|
|
||||||
describe('AuthService test', () => {
|
describe('AuthService test', () => {
|
||||||
|
|
||||||
@@ -150,6 +152,26 @@ describe('AuthService test', () => {
|
|||||||
expect(authService.logout.bind(null)).toThrow();
|
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('', () => {
|
describe('', () => {
|
||||||
|
@@ -18,16 +18,20 @@ import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/emp
|
|||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
|
||||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
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 { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
import { RouteService } from '../services/route.service';
|
import { RouteService } from '../services/route.service';
|
||||||
import { EPersonDataService } from '../eperson/eperson-data.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 LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
|
|
||||||
export const REDIRECT_COOKIE = 'dsRedirectUrl';
|
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
|
* Determines if the user is authenticated
|
||||||
* @returns {Observable<boolean>}
|
* @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() {
|
public checkAuthenticationToken() {
|
||||||
return
|
this.store.dispatch(new CheckAuthenticationTokenAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,8 +206,11 @@ export class AuthService {
|
|||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Accept', 'application/json');
|
headers = headers.append('Accept', 'application/json');
|
||||||
|
if (token && token.accessToken) {
|
||||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||||
|
}
|
||||||
options.headers = headers;
|
options.headers = headers;
|
||||||
|
options.withCredentials = true;
|
||||||
return this.authRequestService.postToEndpoint('login', {}, options).pipe(
|
return this.authRequestService.postToEndpoint('login', {}, options).pipe(
|
||||||
map((status: AuthStatus) => {
|
map((status: AuthStatus) => {
|
||||||
if (status.authenticated) {
|
if (status.authenticated) {
|
||||||
@@ -206,6 +228,18 @@ export class AuthService {
|
|||||||
this.store.dispatch(new ResetAuthenticationMessagesAction());
|
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
|
* Create a new user
|
||||||
* @returns {User}
|
* @returns {User}
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
|
|
||||||
import {take} from 'rxjs/operators';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
|
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';
|
import { select, Store } from '@ngrx/store';
|
||||||
|
|
||||||
// reducers
|
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { isAuthenticated, isAuthenticationLoading } from './selectors';
|
import { isAuthenticated } from './selectors';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { RedirectWhenAuthenticationIsRequiredAction } from './auth.actions';
|
import { RedirectWhenAuthenticationIsRequiredAction } from './auth.actions';
|
||||||
import { isEmpty } from '../../shared/empty.util';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes
|
* Prevent unauthorized activating and loading of routes
|
||||||
|
@@ -12,6 +12,7 @@ import { excludeFromEquals } from '../../utilities/equals.decorators';
|
|||||||
import { AuthError } from './auth-error.model';
|
import { AuthError } from './auth-error.model';
|
||||||
import { AUTH_STATUS } from './auth-status.resource-type';
|
import { AUTH_STATUS } from './auth-status.resource-type';
|
||||||
import { AuthTokenInfo } from './auth-token-info.model';
|
import { AuthTokenInfo } from './auth-token-info.model';
|
||||||
|
import { AuthMethod } from './auth.method';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object that represents the authenticated status of a user
|
* 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
|
* Authentication error if there was one for this status
|
||||||
*/
|
*/
|
||||||
// TODO should be refactored to use the RemoteData error
|
// TODO should be refactored to use the RemoteData error
|
||||||
|
@autoserialize
|
||||||
error?: AuthError;
|
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 _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
|
* Returns the authenticated user
|
||||||
* @function getAuthenticatedUser
|
* @function getAuthenticatedUser
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, take } from 'rxjs/operators';
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { CheckAuthenticationTokenAction } from './auth.actions';
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.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() {
|
public checkAuthenticationCookie(): Observable<AuthStatus> {
|
||||||
this.store.dispatch(new CheckAuthenticationTokenAction())
|
// 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 { CommonModule } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
import {
|
|
||||||
DynamicFormLayoutService,
|
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
DynamicFormService,
|
|
||||||
DynamicFormValidationService
|
|
||||||
} from '@ng-dynamic-forms/core';
|
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
|
||||||
import { Action, StoreConfig, StoreModule } from '@ngrx/store';
|
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 { HostWindowService } from '../shared/host-window.service';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MOCK_RESPONSE_MAP,
|
MOCK_RESPONSE_MAP,
|
||||||
ResponseMapMock,
|
ResponseMapMock,
|
||||||
@@ -47,7 +43,6 @@ import { SubmissionUploadsModel } from './config/models/config-submission-upload
|
|||||||
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
||||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||||
|
|
||||||
import { coreEffects } from './core.effects';
|
import { coreEffects } from './core.effects';
|
||||||
import { coreReducers, CoreState } from './core.reducers';
|
import { coreReducers, CoreState } from './core.reducers';
|
||||||
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
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 { RoleService } from './roles/role.service';
|
||||||
|
|
||||||
import { ApiService } from './services/api.service';
|
import { ApiService } from './services/api.service';
|
||||||
import { RouteService } from './services/route.service';
|
|
||||||
import { ServerResponseService } from './services/server-response.service';
|
import { ServerResponseService } from './services/server-response.service';
|
||||||
import { NativeWindowFactory, NativeWindowService } from './services/window.service';
|
import { NativeWindowFactory, NativeWindowService } from './services/window.service';
|
||||||
import { BitstreamFormat } from './shared/bitstream-format.model';
|
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 { TaskResponseParsingService } from './tasks/task-response-parsing.service';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { storeModuleConfig } from '../app.reducer';
|
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
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -212,7 +210,6 @@ const PROVIDERS = [
|
|||||||
BrowseItemsResponseParsingService,
|
BrowseItemsResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
ConfigResponseParsingService,
|
ConfigResponseParsingService,
|
||||||
RouteService,
|
|
||||||
SubmissionDefinitionsConfigService,
|
SubmissionDefinitionsConfigService,
|
||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigService,
|
||||||
SubmissionRestService,
|
SubmissionRestService,
|
||||||
@@ -256,6 +253,8 @@ const PROVIDERS = [
|
|||||||
RelationshipTypeService,
|
RelationshipTypeService,
|
||||||
ExternalSourceService,
|
ExternalSourceService,
|
||||||
LookupRelationService,
|
LookupRelationService,
|
||||||
|
VersionDataService,
|
||||||
|
VersionHistoryDataService,
|
||||||
LicenseDataService,
|
LicenseDataService,
|
||||||
ItemTypeDataService,
|
ItemTypeDataService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
@@ -306,6 +305,8 @@ export const models =
|
|||||||
ItemType,
|
ItemType,
|
||||||
ExternalSource,
|
ExternalSource,
|
||||||
ExternalSourceEntry,
|
ExternalSourceEntry,
|
||||||
|
Version,
|
||||||
|
VersionHistory
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -230,6 +230,8 @@ export class AuthPostRequest extends PostRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AuthGetRequest extends GetRequest {
|
export class AuthGetRequest extends GetRequest {
|
||||||
|
forceBypassCache = true;
|
||||||
|
|
||||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||||
super(uuid, href, null, options);
|
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;
|
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')) {
|
if (!requestOptions.headers.has('Content-Type')) {
|
||||||
// Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers
|
// 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);
|
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
|
* Service to keep track of the current query parameters
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class RouteService {
|
export class RouteService {
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private store: Store<CoreState>) {
|
constructor(private route: ActivatedRoute, private router: Router, private store: Store<CoreState>) {
|
||||||
this.saveRouting();
|
this.saveRouting();
|
||||||
|
@@ -18,6 +18,8 @@ import { Relationship } from './item-relationships/relationship.model';
|
|||||||
import { RELATIONSHIP } from './item-relationships/relationship.resource-type';
|
import { RELATIONSHIP } from './item-relationships/relationship.resource-type';
|
||||||
import { ITEM } from './item.resource-type';
|
import { ITEM } from './item.resource-type';
|
||||||
import { ChildHALResource } from './child-hal-resource.model';
|
import { ChildHALResource } from './child-hal-resource.model';
|
||||||
|
import { Version } from './version.model';
|
||||||
|
import { VERSION } from './version.resource-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a DSpace Item
|
* Class representing a DSpace Item
|
||||||
@@ -67,6 +69,7 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
|||||||
bundles: HALLink;
|
bundles: HALLink;
|
||||||
owningCollection: HALLink;
|
owningCollection: HALLink;
|
||||||
templateItemOf: HALLink;
|
templateItemOf: HALLink;
|
||||||
|
version: HALLink;
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,6 +80,13 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
|||||||
@link(COLLECTION)
|
@link(COLLECTION)
|
||||||
owningCollection?: Observable<RemoteData<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
|
* The list of Bundles inside this Item
|
||||||
* Will be undefined unless the bundles {@link HALLink} has been resolved.
|
* 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)}">
|
<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>
|
<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>
|
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle
|
||||||
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu aria-labelledby="dropdownLogin">
|
class="px-1">{{ 'nav.login' | translate }}</a>
|
||||||
|
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
||||||
|
aria-labelledby="dropdownLogin">
|
||||||
<ds-log-in
|
<ds-log-in
|
||||||
[isStandalonePage]="false"></ds-log-in>
|
[isStandalonePage]="false"></ds-log-in>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
<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>
|
||||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
||||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
<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">
|
<div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
|
||||||
<ds-user-menu></ds-user-menu>
|
<ds-user-menu></ds-user-menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
<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>
|
</li>
|
||||||
</ul>
|
</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>
|
<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>
|
<div *ngIf="!(loading | async) && !(isAuthenticated | async)" class="px-4 py-3 login-container">
|
||||||
<label for="inputEmail" class="sr-only">{{"login.form.email" | translate}}</label>
|
<ng-container *ngFor="let authMethod of (authMethods | async); let i = index">
|
||||||
<input id="inputEmail"
|
<div *ngIf="i === 1" class="text-center mt-2">
|
||||||
autocomplete="off"
|
<span class="align-middle">{{"login.form.or-divider" | translate}}</span>
|
||||||
autofocus
|
</div>
|
||||||
class="form-control form-control-lg position-relative"
|
<ds-log-in-container [authMethod]="authMethod"></ds-log-in-container>
|
||||||
formControlName="email"
|
</ng-container>
|
||||||
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 class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>
|
<a class="dropdown-item" href="#">{{"login.form.new-user" | translate}}</a>
|
||||||
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
|
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,13 +1,3 @@
|
|||||||
.form-login .form-control:focus {
|
.login-container {
|
||||||
z-index: 2;
|
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 { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
|
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
|
||||||
@@ -10,27 +8,27 @@ import { authReducer } from '../../core/auth/auth.reducer';
|
|||||||
import { EPersonMock } from '../testing/eperson.mock';
|
import { EPersonMock } from '../testing/eperson.mock';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { LogInComponent } from './log-in.component';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
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';
|
import { AppState, storeModuleConfig } from '../../app.reducer';
|
||||||
|
|
||||||
describe('LogInComponent', () => {
|
describe('LogInComponent', () => {
|
||||||
|
|
||||||
let component: LogInComponent;
|
let component: LogInComponent;
|
||||||
let fixture: ComponentFixture<LogInComponent>;
|
let fixture: ComponentFixture<LogInComponent>;
|
||||||
let page: Page;
|
const initialState = {
|
||||||
let user: EPerson;
|
core: {
|
||||||
|
auth: {
|
||||||
const authState = {
|
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
authMethods: authMethodsMock
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
user = EPersonMock;
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
// refine the test module by declaring the test component
|
// refine the test module by declaring the test component
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -43,13 +41,19 @@ describe('LogInComponent', () => {
|
|||||||
strictActionImmutability: false
|
strictActionImmutability: false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
SharedModule,
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
LogInComponent
|
TestComponent
|
||||||
],
|
],
|
||||||
providers: [
|
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: [
|
schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
@@ -59,75 +63,58 @@ describe('LogInComponent', () => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([Store], (store: Store<AppState>) => {
|
describe('', () => {
|
||||||
store
|
let testComp: TestComponent;
|
||||||
.subscribe((state) => {
|
let testFixture: ComponentFixture<TestComponent>;
|
||||||
(state as any).core = Object.create({});
|
|
||||||
(state as any).core.auth = authState;
|
// synchronous beforeEach
|
||||||
|
beforeEach(() => {
|
||||||
|
const html = `<ds-log-in [isStandalonePage]="isStandalonePage"> </ds-log-in>`;
|
||||||
|
|
||||||
|
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||||
|
testComp = testFixture.componentInstance;
|
||||||
});
|
});
|
||||||
|
|
||||||
// create component and test fixture
|
afterEach(() => {
|
||||||
fixture = TestBed.createComponent(LogInComponent);
|
testFixture.destroy();
|
||||||
|
|
||||||
// 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 LogInComponent', inject([LogInComponent], (app: LogInComponent) => {
|
||||||
|
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should create a FormGroup comprised of FormControls', () => {
|
|
||||||
fixture.detectChanges();
|
|
||||||
expect(component.form instanceof FormGroup).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should authenticate', () => {
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LogInComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
// set FormControl values
|
afterEach(() => {
|
||||||
component.form.controls.email.setValue('user');
|
fixture.destroy();
|
||||||
component.form.controls.password.setValue('password');
|
component = null;
|
||||||
|
});
|
||||||
|
|
||||||
// submit form
|
it('should render a log-in container component for each auth method available', () => {
|
||||||
component.submit();
|
const loginContainers = fixture.debugElement.queryAll(By.css('ds-log-in-container'));
|
||||||
|
expect(loginContainers.length).toBe(2);
|
||||||
|
|
||||||
// verify Store.dispatch() is invoked
|
});
|
||||||
expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// declare a test component
|
||||||
* I represent the DOM elements and attach spies.
|
@Component({
|
||||||
*
|
selector: 'ds-test-cmp',
|
||||||
* @class Page
|
template: ``
|
||||||
*/
|
})
|
||||||
class Page {
|
class TestComponent {
|
||||||
|
|
||||||
public emailInput: HTMLInputElement;
|
isStandalonePage = true;
|
||||||
public navigateSpy: jasmine.Spy;
|
|
||||||
public passwordInput: HTMLInputElement;
|
|
||||||
|
|
||||||
constructor(private component: LogInComponent, private fixture: ComponentFixture<LogInComponent>) {
|
|
||||||
// use injector to get services
|
|
||||||
const injector = fixture.debugElement.injector;
|
|
||||||
const store = injector.get(Store);
|
|
||||||
|
|
||||||
// add spies
|
|
||||||
this.navigateSpy = spyOn(store, 'dispatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
public addPageElements() {
|
|
||||||
const emailInputSelector = 'input[formcontrolname=\'email\']';
|
|
||||||
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 { 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 { Observable } from 'rxjs';
|
||||||
import {
|
import { filter, takeWhile, } from 'rxjs/operators';
|
||||||
AuthenticateAction,
|
import { select, Store } from '@ngrx/store';
|
||||||
ResetAuthenticationMessagesAction
|
|
||||||
} from '../../core/auth/auth.actions';
|
|
||||||
|
|
||||||
import {
|
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||||
getAuthenticationError,
|
import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
|
||||||
getAuthenticationInfo,
|
|
||||||
isAuthenticated,
|
|
||||||
isAuthenticationLoading,
|
|
||||||
} from '../../core/auth/selectors';
|
|
||||||
import { CoreState } from '../../core/core.reducers';
|
import { CoreState } from '../../core/core.reducers';
|
||||||
|
|
||||||
import { isNotEmpty } from '../empty.util';
|
|
||||||
import { fadeOut } from '../animations/fade';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /users/sign-in
|
* /users/sign-in
|
||||||
@@ -29,34 +16,21 @@ import { Router } from '@angular/router';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-log-in',
|
selector: 'ds-log-in',
|
||||||
templateUrl: './log-in.component.html',
|
templateUrl: './log-in.component.html',
|
||||||
styleUrls: ['./log-in.component.scss'],
|
styleUrls: ['./log-in.component.scss']
|
||||||
animations: [fadeOut]
|
|
||||||
})
|
})
|
||||||
export class LogInComponent implements OnDestroy, OnInit {
|
export class LogInComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error if authentication fails.
|
* A boolean representing if LogInComponent is in a standalone page
|
||||||
* @type {Observable<string>}
|
|
||||||
*/
|
|
||||||
public error: Observable<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Has authentication error.
|
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
public hasError = false;
|
@Input() isStandalonePage: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The authentication info message.
|
* The list of authentication methods available
|
||||||
* @type {Observable<string>}
|
* @type {AuthMethod[]}
|
||||||
*/
|
*/
|
||||||
public message: Observable<string>;
|
public authMethods: Observable<AuthMethod[]>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Has authentication message.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
public hasMessage = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether user is authenticated.
|
* Whether user is authenticated.
|
||||||
@@ -70,69 +44,28 @@ export class LogInComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
public loading: Observable<boolean>;
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
|
||||||
* The authentication form.
|
|
||||||
* @type {FormGroup}
|
|
||||||
*/
|
|
||||||
public form: FormGroup;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component state.
|
* Component state.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
private alive = true;
|
private alive = true;
|
||||||
|
|
||||||
@Input() isStandalonePage: boolean;
|
constructor(private store: Store<CoreState>,
|
||||||
|
private authService: AuthService,) {
|
||||||
/**
|
|
||||||
* @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>
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
ngOnInit(): void {
|
||||||
* 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));
|
|
||||||
|
|
||||||
// set formGroup
|
this.authMethods = this.store.pipe(
|
||||||
this.form = this.formBuilder.group({
|
select(getAuthenticationMethods),
|
||||||
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;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// set loading
|
// set loading
|
||||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||||
|
|
||||||
|
// set isAuthenticated
|
||||||
|
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||||
|
|
||||||
// subscribe to success
|
// subscribe to success
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(isAuthenticated),
|
select(isAuthenticated),
|
||||||
@@ -142,55 +75,11 @@ export class LogInComponent implements OnDestroy, OnInit {
|
|||||||
this.authService.redirectAfterLoginSuccess(this.isStandalonePage);
|
this.authService.redirectAfterLoginSuccess(this.isStandalonePage);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
ngOnDestroy(): void {
|
||||||
* Lifecycle hook that is called when a directive, pipe or service is destroyed.
|
|
||||||
* @method ngOnDestroy
|
|
||||||
*/
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.alive = false;
|
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;
|
@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.
|
* Current page.
|
||||||
*/
|
*/
|
||||||
@@ -173,20 +180,35 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
this.checkConfig(this.paginationOptions);
|
this.checkConfig(this.paginationOptions);
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
// Listen to changes
|
// Listen to changes
|
||||||
|
if (!this.disableRouteParameterUpdate) {
|
||||||
this.subs.push(this.route.queryParams
|
this.subs.push(this.route.queryParams
|
||||||
.subscribe((queryParams) => {
|
.subscribe((queryParams) => {
|
||||||
if (this.isEmptyPaginationParams(queryParams)) {
|
this.initializeParams(queryParams);
|
||||||
this.initializeConfig(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 {
|
} else {
|
||||||
this.currentQueryParams = queryParams;
|
this.currentQueryParams = params;
|
||||||
const fixedProperties = this.validateParams(queryParams);
|
const fixedProperties = this.validateParams(params);
|
||||||
if (isNotEmpty(fixedProperties)) {
|
if (isNotEmpty(fixedProperties)) {
|
||||||
|
if (!this.disableRouteParameterUpdate) {
|
||||||
this.fixRoute(fixedProperties);
|
this.fixRoute(fixedProperties);
|
||||||
|
} else {
|
||||||
|
this.initializeParams(fixedProperties);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setFields();
|
this.setFields();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fixRoute(fixedProperties) {
|
private fixRoute(fixedProperties) {
|
||||||
@@ -247,7 +269,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
* The page being navigated to.
|
* The page being navigated to.
|
||||||
*/
|
*/
|
||||||
public doPageChange(page: number) {
|
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.
|
* The page size being navigated to.
|
||||||
*/
|
*/
|
||||||
public doPageSizeChange(pageSize: number) {
|
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.
|
* The sort direction being navigated to.
|
||||||
*/
|
*/
|
||||||
public doSortDirectionChange(sortDirection: SortDirection) {
|
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.
|
* The sort field being navigated to.
|
||||||
*/
|
*/
|
||||||
public doSortFieldChange(field: string) {
|
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
|
* 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 { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||||
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
||||||
import { VarDirective } from './utils/var.directive';
|
import { VarDirective } from './utils/var.directive';
|
||||||
import { LogInComponent } from './log-in/log-in.component';
|
|
||||||
import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
|
import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
|
||||||
import { LogOutComponent } from './log-out/log-out.component';
|
import { LogOutComponent } from './log-out/log-out.component';
|
||||||
import { FormComponent } from './form/form.component';
|
import { FormComponent } from './form/form.component';
|
||||||
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.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 { 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 { 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 { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
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 { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
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 { 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 { 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 { MissingTranslationHelper } from './translate/missing-translation.helper';
|
||||||
|
import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -344,7 +352,12 @@ const COMPONENTS = [
|
|||||||
ExternalSourceEntryImportModalComponent,
|
ExternalSourceEntryImportModalComponent,
|
||||||
ImportableListItemControlComponent,
|
ImportableListItemControlComponent,
|
||||||
ExistingMetadataListElementComponent,
|
ExistingMetadataListElementComponent,
|
||||||
|
LogInShibbolethComponent,
|
||||||
|
LogInPasswordComponent,
|
||||||
|
LogInContainerComponent,
|
||||||
|
ItemVersionsComponent,
|
||||||
PublicationSearchResultListElementComponent,
|
PublicationSearchResultListElementComponent,
|
||||||
|
ItemVersionsNoticeComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -408,6 +421,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
DsDynamicLookupRelationSelectionTabComponent,
|
DsDynamicLookupRelationSelectionTabComponent,
|
||||||
DsDynamicLookupRelationExternalSourceTabComponent,
|
DsDynamicLookupRelationExternalSourceTabComponent,
|
||||||
ExternalSourceEntryImportModalComponent,
|
ExternalSourceEntryImportModalComponent,
|
||||||
|
LogInPasswordComponent,
|
||||||
|
LogInShibbolethComponent,
|
||||||
|
ItemVersionsComponent,
|
||||||
|
ItemVersionsNoticeComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||||
|
@@ -23,7 +23,7 @@ export class AuthRequestServiceStub {
|
|||||||
} else {
|
} else {
|
||||||
authStatusStub.authenticated = false;
|
authStatusStub.authenticated = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (isNotEmpty(options)) {
|
||||||
const token = (options.headers as any).lazyUpdate[1].value;
|
const token = (options.headers as any).lazyUpdate[1].value;
|
||||||
if (this.validateToken(token)) {
|
if (this.validateToken(token)) {
|
||||||
authStatusStub.authenticated = true;
|
authStatusStub.authenticated = true;
|
||||||
@@ -32,6 +32,8 @@ export class AuthRequestServiceStub {
|
|||||||
} else {
|
} else {
|
||||||
authStatusStub.authenticated = false;
|
authStatusStub.authenticated = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
authStatusStub.authenticated = false;
|
||||||
}
|
}
|
||||||
return observableOf(authStatusStub);
|
return observableOf(authStatusStub);
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,7 @@ export class AuthRequestServiceStub {
|
|||||||
authStatusStub.authenticated = false;
|
authStatusStub.authenticated = false;
|
||||||
break;
|
break;
|
||||||
case 'status':
|
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)) {
|
if (this.validateToken(token)) {
|
||||||
authStatusStub.authenticated = true;
|
authStatusStub.authenticated = true;
|
||||||
authStatusStub.token = this.mockTokenInfo;
|
authStatusStub.token = this.mockTokenInfo;
|
||||||
|
@@ -4,6 +4,12 @@ import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
|||||||
import { EPersonMock } from './eperson.mock';
|
import { EPersonMock } from './eperson.mock';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
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 {
|
export class AuthServiceStub {
|
||||||
|
|
||||||
@@ -106,4 +112,12 @@ export class AuthServiceStub {
|
|||||||
isAuthenticated() {
|
isAuthenticated() {
|
||||||
return observableOf(true);
|
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.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.head": "View Item",
|
||||||
|
|
||||||
"item.edit.tabs.view.title": "Item Edit - View",
|
"item.edit.tabs.view.title": "Item Edit - View",
|
||||||
@@ -1084,6 +1090,29 @@
|
|||||||
"item.select.table.title": "Title",
|
"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",
|
"journal.listelement.badge": "Journal",
|
||||||
|
|
||||||
@@ -1175,8 +1204,12 @@
|
|||||||
|
|
||||||
"login.form.new-user": "New user? Click here to register.",
|
"login.form.new-user": "New user? Click here to register.",
|
||||||
|
|
||||||
|
"login.form.or-divider": "or",
|
||||||
|
|
||||||
"login.form.password": "Password",
|
"login.form.password": "Password",
|
||||||
|
|
||||||
|
"login.form.shibboleth": "Log in with Shibboleth",
|
||||||
|
|
||||||
"login.form.submit": "Log in",
|
"login.form.submit": "Log in",
|
||||||
|
|
||||||
"login.title": "Login",
|
"login.title": "Login",
|
||||||
|
@@ -918,7 +918,7 @@
|
|||||||
"item.edit.move.processing": "Movendo...",
|
"item.edit.move.processing": "Movendo...",
|
||||||
|
|
||||||
// "item.edit.move.search.placeholder": "Enter a search query to look for collections",
|
// "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": "The item has been moved successfully",
|
||||||
"item.edit.move.success": "O item foi movido com sucesso",
|
"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 { ItemPageConfig } from './item-page-config.interface';
|
||||||
import { CollectionPageConfig } from './collection-page-config.interface';
|
import { CollectionPageConfig } from './collection-page-config.interface';
|
||||||
import { Theme } from './theme.inferface';
|
import { Theme } from './theme.inferface';
|
||||||
|
import {AuthConfig} from './auth-config.interfaces';
|
||||||
|
|
||||||
export interface GlobalConfig extends Config {
|
export interface GlobalConfig extends Config {
|
||||||
ui: ServerConfig;
|
ui: ServerConfig;
|
||||||
rest: ServerConfig;
|
rest: ServerConfig;
|
||||||
production: boolean;
|
production: boolean;
|
||||||
cache: CacheConfig;
|
cache: CacheConfig;
|
||||||
|
auth: AuthConfig;
|
||||||
form: FormConfig;
|
form: FormConfig;
|
||||||
notifications: INotificationBoardOptions;
|
notifications: INotificationBoardOptions;
|
||||||
submission: SubmissionConfig;
|
submission: SubmissionConfig;
|
||||||
|
Reference in New Issue
Block a user