mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Prototype ready
This commit is contained in:
@@ -97,6 +97,7 @@
|
||||
"login.form.new-user": "Sind Sie neu hier? Klicken Sie hier, um sich zu registrieren.",
|
||||
"login.form.password": "Passwort",
|
||||
"login.form.submit": "Einloggen",
|
||||
"login.form.ssoLogin": "Shibboleth",
|
||||
"login.title": "Einloggen",
|
||||
"logout.form.header": "Ausloggen aus DSpace",
|
||||
"logout.form.submit": "Ausloggen",
|
||||
|
@@ -353,7 +353,7 @@
|
||||
"login.form.new-user": "New user? Click here to register.",
|
||||
"login.form.password": "Password",
|
||||
"login.form.submit": "Log in",
|
||||
"login.shibbForm.submit": "Shibboleth",
|
||||
"login.form.ssoLogin": "Shibboleth",
|
||||
"login.title": "Login",
|
||||
"logout.form.header": "Log out from DSpace",
|
||||
"logout.form.submit": "Log out",
|
||||
|
@@ -0,0 +1,3 @@
|
||||
export class ShibbConstants {
|
||||
public static readonly SHIBBOLETH_REDIRECT_ROUTE = 'shibboleth';
|
||||
}
|
@@ -1,20 +1,20 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ShibbolethComponent } from './shibboleth.component';
|
||||
import { ShibbolethTargetPageComponent } from './shibboleth-target-page.component';
|
||||
|
||||
describe('ShibbolethComponent', () => {
|
||||
let component: ShibbolethComponent;
|
||||
let fixture: ComponentFixture<ShibbolethComponent>;
|
||||
let component: ShibbolethTargetPageComponent;
|
||||
let fixture: ComponentFixture<ShibbolethTargetPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ShibbolethComponent ]
|
||||
declarations: [ ShibbolethTargetPageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ShibbolethComponent);
|
||||
fixture = TestBed.createComponent(ShibbolethTargetPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@@ -6,10 +6,10 @@ import { Observable, of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-shibboleth-page',
|
||||
templateUrl: './shibboleth.component.html',
|
||||
styleUrls: ['./shibboleth.component.scss']
|
||||
templateUrl: './shibboleth-target-page.component.html',
|
||||
styleUrls: ['./shibboleth-target-page.component.scss']
|
||||
})
|
||||
export class ShibbolethComponent implements OnInit {
|
||||
export class ShibbolethTargetPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* True if the shibboleth authentication is loading.
|
@@ -3,7 +3,8 @@ import { RouterModule } from '@angular/router';
|
||||
|
||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||
import { ShibbolethComponent } from './+login-page/shibbolethTargetPage/shibboleth.component';
|
||||
import { ShibbolethTargetPageComponent } from './+login-page/shibbolethTargetPage/shibboleth-target-page.component';
|
||||
import { ShibbConstants } from './+login-page/shibbolethTargetPage/const/shibbConstants';
|
||||
|
||||
const ITEM_MODULE_PATH = 'items';
|
||||
export function getItemModulePath() {
|
||||
@@ -40,7 +41,7 @@ export function getAdminModulePath() {
|
||||
{ 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: 'shibboleth', pathMatch: 'full', component: ShibbolethComponent },
|
||||
{ path: ShibbConstants.SHIBBOLETH_REDIRECT_ROUTE, pathMatch: 'full', component: ShibbolethTargetPageComponent },
|
||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||
])
|
||||
],
|
||||
|
@@ -39,7 +39,7 @@ import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/e
|
||||
import { NavbarModule } from './navbar/navbar.module';
|
||||
import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
|
||||
import { ShibbolethComponent } from './+login-page/shibbolethTargetPage/shibboleth.component';
|
||||
import { ShibbolethTargetPageComponent } from './+login-page/shibbolethTargetPage/shibboleth-target-page.component';
|
||||
|
||||
export function getConfig() {
|
||||
return ENV_CONFIG;
|
||||
@@ -113,7 +113,7 @@ const DECLARATIONS = [
|
||||
PageNotFoundComponent,
|
||||
NotificationComponent,
|
||||
NotificationsBoardComponent,
|
||||
ShibbolethComponent
|
||||
ShibbolethTargetPageComponent
|
||||
];
|
||||
|
||||
const EXPORTS = [
|
||||
|
@@ -5,7 +5,8 @@ import { Injectable, Injector } from '@angular/core';
|
||||
import {
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
HttpHandler, HttpHeaders,
|
||||
HttpHandler,
|
||||
HttpHeaders,
|
||||
HttpInterceptor,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
@@ -17,12 +18,12 @@ import { AppState } from '../../app.reducer';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { isNotEmpty, isUndefined, isNotNull } from '../../shared/empty.util';
|
||||
import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthError } from './models/auth-error.model';
|
||||
import { AuthMethodModel } from './models/auth-method.model';
|
||||
import { AuthMethodType } from '../../shared/log-in/methods/authMethods-type';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
@@ -72,14 +73,31 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
return parsedLocation;
|
||||
}
|
||||
|
||||
private sortAuthMethods(authMethodModels: AuthMethodModel[]): AuthMethodModel[] {
|
||||
const sortedAuthMethodModels: AuthMethodModel[] = new Array<AuthMethodModel>();
|
||||
authMethodModels.forEach((method) => {
|
||||
if (method.authMethodType === AuthMethodType.Password) {
|
||||
sortedAuthMethodModels.push(method);
|
||||
}
|
||||
});
|
||||
|
||||
authMethodModels.forEach((method) => {
|
||||
if (method.authMethodType !== AuthMethodType.Password) {
|
||||
sortedAuthMethodModels.push(method);
|
||||
}
|
||||
});
|
||||
|
||||
return sortedAuthMethodModels;
|
||||
}
|
||||
|
||||
private parseAuthMethodsfromHeaders(headers: HttpHeaders): AuthMethodModel[] {
|
||||
const authMethodModels: AuthMethodModel[] = [];
|
||||
let authMethodModels: AuthMethodModel[] = [];
|
||||
const parts: string[] = headers.get('www-authenticate').split(',');
|
||||
// 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);
|
||||
console.log('realms: ', realms)
|
||||
// console.log('realms: ', realms)
|
||||
|
||||
// tslint:disable-next-line:forin
|
||||
for (const j in realms) {
|
||||
@@ -87,26 +105,28 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
|
||||
const splittedRealm = realms[j].split(', ');
|
||||
const methodName = splittedRealm[0].split(' ')[0].trim();
|
||||
console.log('methodName: ', methodName);
|
||||
// console.log('methodName: ', methodName);
|
||||
|
||||
console.log('splittedRealm: ', splittedRealm);
|
||||
// console.log('splittedRealm: ', splittedRealm);
|
||||
let authMethodModel: AuthMethodModel;
|
||||
if (splittedRealm.length === 1) {
|
||||
authMethodModel = new AuthMethodModel(methodName);
|
||||
authMethodModels.push(authMethodModel);
|
||||
} else if (splittedRealm.length > 1) {
|
||||
authMethodModel = new AuthMethodModel(methodName);
|
||||
let location = splittedRealm[1];
|
||||
location = this.parseLocation(location)
|
||||
authMethodModel.location = location;
|
||||
console.log('location: ', location);
|
||||
location = this.parseLocation(location);
|
||||
authMethodModel = new AuthMethodModel(methodName, location);
|
||||
// console.log('location: ', location);
|
||||
authMethodModels.push(authMethodModel);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the email + password login component gets rendered first
|
||||
authMethodModels = this.sortAuthMethods(authMethodModels);
|
||||
return authMethodModels;
|
||||
}
|
||||
|
||||
private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string, httpHeaders?: HttpHeaders): AuthStatus {
|
||||
private makeAuthStatusObject(authenticated: boolean, accessToken ?: string, error ?: string, httpHeaders ?: HttpHeaders): AuthStatus {
|
||||
const authStatus = new AuthStatus();
|
||||
// let authMethods: AuthMethodModel[];
|
||||
if (httpHeaders) {
|
||||
@@ -135,7 +155,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
const token = authService.getToken();
|
||||
let newReq;
|
||||
|
||||
// console.log('intercept() request: ', req);
|
||||
// console.log('intercept() request: ', req);
|
||||
|
||||
if (authService.isTokenExpired()) {
|
||||
authService.setRedirectUrl(this.router.url);
|
||||
@@ -163,7 +183,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
newReq = req;
|
||||
}
|
||||
|
||||
// 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(
|
||||
// tap((response) => console.log('next.handle: ', response)),
|
||||
map((response) => {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import {AuthMethodType} from '../../../shared/log-in/methods/authMethods-type';
|
||||
import { ShibbConstants } from '../../../+login-page/shibbolethTargetPage/const/shibbConstants';
|
||||
|
||||
export class AuthMethodModel {
|
||||
authMethodType: AuthMethodType;
|
||||
location?: string;
|
||||
|
||||
constructor(authMethodName: string, location?: string) {
|
||||
this.location = location;
|
||||
switch (authMethodName) {
|
||||
case 'ip': {
|
||||
this.authMethodType = AuthMethodType.Ip;
|
||||
@@ -17,6 +17,12 @@ export class AuthMethodModel {
|
||||
}
|
||||
case 'shibboleth': {
|
||||
this.authMethodType = AuthMethodType.Shibboleth;
|
||||
const strings: string[] = location.split('target=');
|
||||
const target = strings[1];
|
||||
|
||||
console.log('strings', strings);
|
||||
|
||||
this.location = target + location + '/' + ShibbConstants.SHIBBOLETH_REDIRECT_ROUTE;
|
||||
break;
|
||||
}
|
||||
case 'x509': {
|
||||
|
@@ -107,7 +107,6 @@ const _getRegistrationError = (state: AuthState) => state.error;
|
||||
*/
|
||||
const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
|
||||
|
||||
// @Art: these two are the ones i added:
|
||||
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
||||
|
||||
/**
|
||||
|
@@ -3,7 +3,7 @@ import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { Observable, race as observableRace } from 'rxjs';
|
||||
import { filter, find, map, mergeMap, take } from 'rxjs/operators';
|
||||
import { filter, map, mergeMap, take } from 'rxjs/operators';
|
||||
import { cloneDeep, remove } from 'lodash';
|
||||
|
||||
import { AppState } from '../../app.reducer';
|
||||
@@ -65,8 +65,7 @@ const uuidsFromHrefSubstringSelector =
|
||||
const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => {
|
||||
let result = [];
|
||||
if (isNotEmpty(state)) {
|
||||
result = Object.values(state)
|
||||
.filter((value: string) => value.startsWith(href));
|
||||
result = Object.keys(state).filter((key) => key.startsWith(href)).map((key) => state[key]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -263,8 +262,9 @@ export class RequestService {
|
||||
*/
|
||||
private clearRequestsOnTheirWayToTheStore(request: GetRequest) {
|
||||
this.getByHref(request.href).pipe(
|
||||
find((re: RequestEntry) => hasValue(re)))
|
||||
.subscribe((re: RequestEntry) => {
|
||||
filter((re: RequestEntry) => hasValue(re)),
|
||||
take(1)
|
||||
).subscribe((re: RequestEntry) => {
|
||||
if (!re.responsePending) {
|
||||
remove(this.requestsOnTheirWayToTheStore, (item) => item === request.href);
|
||||
}
|
||||
@@ -315,4 +315,15 @@ export class RequestService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an observable that emits a new value whenever the availability of the cached request changes.
|
||||
* The value it emits is a boolean stating if the request exists in cache or not.
|
||||
* @param href The href of the request to observe
|
||||
*/
|
||||
hasByHrefObservable(href: string): Observable<boolean> {
|
||||
return this.getByHref(href).pipe(
|
||||
map((requestEntry: RequestEntry) => this.isValid(requestEntry))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
AuthenticateAction,
|
||||
ResetAuthenticationMessagesAction, GetJWTafterShibbLoginAction
|
||||
ResetAuthenticationMessagesAction
|
||||
} from '../../../../core/auth/auth.actions';
|
||||
|
||||
import {
|
||||
|
@@ -1,19 +0,0 @@
|
||||
<form *ngIf="!(loading | async) && !(isAuthenticated | async)" class="form-login px-4 py-3" (ngSubmit)="submit()"
|
||||
[formGroup]="shibbForm" novalidate>
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit"
|
||||
[disabled]="!shibbForm.valid"
|
||||
>{{"login.shibbForm.submit" | translate}}</button>
|
||||
</form>
|
||||
|
||||
<!--
|
||||
<div *ngIf="!(loading | async) && !(isAuthenticated | async)" class="form-login px-4 py-3">
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3"
|
||||
type="submit"
|
||||
[formControl]="shibbButton"
|
||||
(click)="submit()"
|
||||
>{{"login.shibbForm.submit" | translate}}</button>
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
|
@@ -1,13 +0,0 @@
|
||||
.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,9 @@
|
||||
<div class="form-login px-4 py-3 shibb" *ngIf="!(loading | async) && !(isAuthenticated | async)">
|
||||
<a class="btn btn-lg btn-primary btn-block mt-3"
|
||||
(click)="submit()"
|
||||
role="button"
|
||||
>{{"login.form.ssoLogin" | translate}}</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
.shibb {
|
||||
color: #FFFFFF;
|
||||
}
|
@@ -1,4 +1,14 @@
|
||||
import { Component, EventEmitter, Inject, Input, OnInit, Output, QueryList, ViewChildren } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { renderAuthMethodFor } from '../authMethods-decorator';
|
||||
import { AuthMethodType } from '../authMethods-type';
|
||||
import { AuthMethodModel } from '../../../../core/auth/models/auth-method.model';
|
||||
@@ -6,19 +16,25 @@ import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { CoreState } from '../../../../core/core.reducers';
|
||||
import { StartShibbolethAuthenticationAction } from '../../../../core/auth/auth.actions';
|
||||
import { Observable } from 'rxjs';
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
||||
import { Observable, of, Subscription } from 'rxjs';
|
||||
import {
|
||||
getAuthenticationMethods,
|
||||
isAuthenticated,
|
||||
isAuthenticationLoading
|
||||
} from '../../../../core/auth/selectors';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../../config';
|
||||
import { ShibbConstants } from '../../../../+login-page/shibbolethTargetPage/const/shibbConstants';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-shibboleth',
|
||||
templateUrl: './dynamic-shibboleth.component.html',
|
||||
styleUrls: ['./dynamic-shibboleth.component.scss'],
|
||||
selector: 'ds-log-in-shibboleth',
|
||||
templateUrl: './log-in-shibboleth.component.html',
|
||||
styleUrls: ['./log-in-shibboleth.component.scss'],
|
||||
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
||||
export class DynamicShibbolethComponent implements OnInit {
|
||||
export class LogInShibbolethComponent implements OnInit {
|
||||
|
||||
@Input() authMethodModel: AuthMethodModel;
|
||||
|
||||
@@ -34,38 +50,18 @@ export class DynamicShibbolethComponent implements OnInit {
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The authentication form.
|
||||
* @type {FormGroup}
|
||||
*/
|
||||
public shibbForm: FormGroup;
|
||||
|
||||
private host: string;
|
||||
|
||||
// public shibbButton: FormControl;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor(@Inject('authMethodModelProvider') public injectedAuthMethodModel: AuthMethodModel,
|
||||
@Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig,
|
||||
private formBuilder: FormBuilder,
|
||||
private store: Store<CoreState>) {
|
||||
this.authMethodModel = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('conf: ',this.envConfig.rest.host);
|
||||
|
||||
this.host = this.envConfig.rest.host;
|
||||
|
||||
// console.log('injectedAuthMethodModel', this.injectedAuthMethodModel);
|
||||
// set formGroup
|
||||
this.shibbForm = this.formBuilder.group({
|
||||
shibbButton: [''],
|
||||
});
|
||||
|
||||
// this.shibbButton = new FormControl('');
|
||||
// console.log('Injected authMethodModel', this.injectedAuthMethodModel);
|
||||
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
@@ -78,8 +74,10 @@ export class DynamicShibbolethComponent implements OnInit {
|
||||
submit() {
|
||||
console.log('submit() was called');
|
||||
this.store.dispatch(new StartShibbolethAuthenticationAction(this.authMethodModel));
|
||||
this.host = 'fis.tiss.tuwien.ac.at';
|
||||
// https://dspace.hostname/Shibboleth.sso/Login?target=https://dspace.hostname/shibboleth
|
||||
window.location.href = 'https://' + this.host + '/Shibboleth.sso/Login?target=https://' + this.host + '/shibboleth';
|
||||
// e.g. host = 'fis.tiss.tuwien.ac.at';
|
||||
// https://host/Shibboleth.sso/Login?target=https://host/shibboleth
|
||||
// https://fis.tiss.tuwien.ac.at/Shibboleth.sso/Login?target=https://fis.tiss.tuwien.ac.at/shibboleth';
|
||||
window.location.href = this.injectedAuthMethodModel.location;
|
||||
}
|
||||
|
||||
}
|
@@ -137,7 +137,7 @@ import { RoleDirective } from './roles/role.directive';
|
||||
import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component';
|
||||
import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component';
|
||||
import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
|
||||
import {DynamicShibbolethComponent} from './log-in/methods/shibboleth/dynamic-shibboleth.component';
|
||||
import {LogInShibbolethComponent} from './log-in/methods/shibboleth/log-in-shibboleth.component';
|
||||
// import {LogInComponent} from './log-in/log-in.component';
|
||||
import {LogInPasswordComponent} from './log-in/methods/password/log-in-password.component';
|
||||
import { LoginContainerComponent } from './log-in/container/login-container.component';
|
||||
@@ -263,7 +263,7 @@ const COMPONENTS = [
|
||||
ItemTypeSwitcherComponent,
|
||||
BrowseByComponent,
|
||||
// LogInComponent,
|
||||
DynamicShibbolethComponent,
|
||||
LogInShibbolethComponent,
|
||||
LogInPasswordComponent,
|
||||
LoginContainerComponent,
|
||||
LogInComponent
|
||||
@@ -311,7 +311,7 @@ const ENTRY_COMPONENTS = [
|
||||
ItemMetadataListElementComponent,
|
||||
MetadataRepresentationListElementComponent,
|
||||
LogInPasswordComponent,
|
||||
DynamicShibbolethComponent
|
||||
LogInShibbolethComponent
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
|
Reference in New Issue
Block a user