[CST-15073][CST-15074] Adaptation with standalone components

This commit is contained in:
Alisa Ismailati
2024-09-24 13:00:29 +02:00
committed by Vincenzo Mecca
parent 214a77a65c
commit 83615a1c90
63 changed files with 1211 additions and 786 deletions

View File

@@ -1,4 +1,8 @@
import { InMemoryScrollingOptions, Route, RouterConfigOptions, } from '@angular/router'; import {
InMemoryScrollingOptions,
Route,
RouterConfigOptions,
} from '@angular/router';
import { NOTIFICATIONS_MODULE_PATH } from './admin/admin-routing-paths'; import { NOTIFICATIONS_MODULE_PATH } from './admin/admin-routing-paths';
import { import {
@@ -21,12 +25,8 @@ import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routin
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
import { authBlockingGuard } from './core/auth/auth-blocking.guard'; import { authBlockingGuard } from './core/auth/auth-blocking.guard';
import { authenticatedGuard } from './core/auth/authenticated.guard'; import { authenticatedGuard } from './core/auth/authenticated.guard';
import { import { groupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
groupAdministratorGuard import { siteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
} from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import {
siteAdministratorGuard
} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { siteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; import { siteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
import { endUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard'; import { endUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
import { reloadGuard } from './core/reload/reload.guard'; import { reloadGuard } from './core/reload/reload.guard';
@@ -37,9 +37,7 @@ import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
import { menuResolver } from './menuResolver'; import { menuResolver } from './menuResolver';
import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state'; import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
import { import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
ThemedPageInternalServerErrorComponent
} from './page-internal-server-error/themed-page-internal-server-error.component';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths'; import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
import { provideSubmissionState } from './submission/provide-submission-state'; import { provideSubmissionState } from './submission/provide-submission-state';
@@ -261,17 +259,17 @@ export const APP_ROUTES: Route[] = [
}, },
{ {
path: 'external-login/:token', path: 'external-login/:token',
loadChildren: () => import('./external-login-page/external-login-routes').then((m) => m.ROUTES) loadChildren: () => import('./external-login-page/external-login-routes').then((m) => m.ROUTES),
}, },
{ {
path: 'review-account/:token', path: 'review-account/:token',
loadChildren: () => import('./external-login-review-account-info-page/external-login-review-account-info-page-routes') loadChildren: () => import('./external-login-review-account-info-page/external-login-review-account-info-page-routes')
.then((m) => m.ROUTES) .then((m) => m.ROUTES),
}, },
{ {
path: 'email-confirmation', path: 'email-confirmation',
loadChildren: () => import('./external-login-email-confirmation-page/external-login-email-confirmation-page-routes') loadChildren: () => import('./external-login-email-confirmation-page/external-login-email-confirmation-page-routes')
.then((m) => m.ROUTES) .then((m) => m.ROUTES),
}, },
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
], ],

View File

@@ -1,22 +1,36 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap, take, tap, } from 'rxjs/operators'; import {
distinctUntilChanged,
filter,
map,
switchMap,
take,
tap,
} from 'rxjs/operators';
import { isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import {
isNotEmpty,
isNotEmptyOperator,
} from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { DeleteRequest, GetRequest, PostRequest } from '../data/request.models';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import {
DeleteRequest,
GetRequest,
PostRequest,
} from '../data/request.models';
import { RequestService } from '../data/request.service';
import { RestRequest } from '../data/rest-request.model'; import { RestRequest } from '../data/rest-request.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { sendRequest } from '../shared/request.operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { ShortLivedToken } from './models/short-lived-token.model';
import { MachineToken } from './models/machine-token.model'; import { MachineToken } from './models/machine-token.model';
import { NoContent } from '../shared/NoContent.model'; import { ShortLivedToken } from './models/short-lived-token.model';
import { sendRequest } from '../shared/request.operators';
/** /**
* Abstract service to send authentication requests * Abstract service to send authentication requests
@@ -144,7 +158,7 @@ export abstract class AuthRequestService {
map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()), map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
tap((request: RestRequest) => this.requestService.send(request)), tap((request: RestRequest) => this.requestService.send(request)),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<MachineToken>(request.uuid)) switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<MachineToken>(request.uuid)),
); );
} }

View File

@@ -1,14 +1,33 @@
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { Inject, Injectable, Optional, } from '@angular/core'; import {
Inject,
Injectable,
Optional,
} from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { select, Store, } from '@ngrx/store'; import {
select,
Store,
} from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
import { Observable, of as observableOf, } from 'rxjs'; import {
import { filter, map, startWith, switchMap, take, } from 'rxjs/operators'; Observable,
of as observableOf,
} from 'rxjs';
import {
filter,
map,
startWith,
switchMap,
take,
} from 'rxjs/operators';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { REQUEST, RESPONSE, } from '../../../express.tokens'; import {
REQUEST,
RESPONSE,
} from '../../../express.tokens';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { import {
hasNoValue, hasNoValue,
@@ -22,7 +41,10 @@ import {
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
import { buildPaginatedList, PaginatedList, } from '../data/paginated-list.model'; import {
buildPaginatedList,
PaginatedList,
} from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { EPersonDataService } from '../eperson/eperson-data.service'; import { EPersonDataService } from '../eperson/eperson-data.service';
@@ -32,17 +54,16 @@ import { CookieService } from '../services/cookie.service';
import { HardRedirectService } from '../services/hard-redirect.service'; import { HardRedirectService } from '../services/hard-redirect.service';
import { RouteService } from '../services/route.service'; import { RouteService } from '../services/route.service';
import { import {
getAuthenticatedUserId, NativeWindowRef,
getAuthenticationToken, NativeWindowService,
getExternalAuthCookieStatus, } from '../services/window.service';
getRedirectUrl, import { NoContent } from '../shared/NoContent.model';
isAuthenticated, import {
isAuthenticatedLoaded, getAllSucceededRemoteDataPayload,
isIdle, getFirstCompletedRemoteData,
isTokenRefreshing } from '../shared/operators';
} from './selectors';
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../shared/operators';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { URLCombiner } from '../url-combiner/url-combiner';
import { import {
CheckAuthenticationTokenAction, CheckAuthenticationTokenAction,
RefreshTokenAction, RefreshTokenAction,
@@ -55,11 +76,21 @@ import {
import { AuthRequestService } from './auth-request.service'; import { AuthRequestService } from './auth-request.service';
import { AuthMethod } from './models/auth.method'; import { AuthMethod } from './models/auth.method';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo, TOKENITEM, } from './models/auth-token-info.model'; import {
import { NoContent } from '../shared/NoContent.model'; AuthTokenInfo,
import { URLCombiner } from '../url-combiner/url-combiner'; TOKENITEM,
} from './models/auth-token-info.model';
import { MachineToken } from './models/machine-token.model'; import { MachineToken } from './models/machine-token.model';
import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import {
getAuthenticatedUserId,
getAuthenticationToken,
getExternalAuthCookieStatus,
getRedirectUrl,
isAuthenticated,
isAuthenticatedLoaded,
isIdle,
isTokenRefreshing,
} from './selectors';
export const LOGIN_ROUTE = '/login'; export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout'; export const LOGOUT_ROUTE = '/logout';

View File

@@ -1,10 +1,14 @@
import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; import {
autoserialize,
autoserializeAs,
deserialize,
} from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators'; import { typedObject } from '../../cache/builders/build-decorators';
import { CacheableObject } from '../../cache/cacheable-object.model'; import { CacheableObject } from '../../cache/cacheable-object.model';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { ResourceType } from '../../shared/resource-type';
import { HALLink } from '../../shared/hal-link.model'; import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { MACHINE_TOKEN } from './machine-token.resource-type'; import { MACHINE_TOKEN } from './machine-token.resource-type';
/** /**

View File

@@ -1,22 +1,36 @@
import { HttpHeaders, HttpParams, } from '@angular/common/http'; import {
HttpHeaders,
HttpParams,
} from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RequestService } from './request.service'; import { Operation } from 'fast-json-patch';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { GetRequest, PatchRequest, PostRequest } from './request.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, find, map, } from 'rxjs/operators'; import {
filter,
find,
map,
} from 'rxjs/operators';
import { hasValue, isNotEmpty, } from '../../shared/empty.util'; import {
hasValue,
isNotEmpty,
} from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
import { Registration } from '../shared/registration.model'; import { Registration } from '../shared/registration.model';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RegistrationResponseParsingService } from './registration-response-parsing.service'; import { RegistrationResponseParsingService } from './registration-response-parsing.service';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { Operation } from 'fast-json-patch'; import {
import { NoContent } from '../shared/NoContent.model'; GetRequest,
PatchRequest,
PostRequest,
} from './request.models';
import { RequestService } from './request.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -116,10 +130,10 @@ export class EpersonRegistrationService {
map((rd) => { map((rd) => {
if (rd.hasSucceeded && hasValue(rd.payload)) { if (rd.hasSucceeded && hasValue(rd.payload)) {
return Object.assign(rd, { payload: Object.assign(new Registration(), { return Object.assign(rd, { payload: Object.assign(new Registration(), {
email: rd.payload.email, email: rd.payload.email,
token: token, token: token,
user: rd.payload.user, user: rd.payload.user,
}) }); }) });
} else { } else {
return rd; return rd;
} }
@@ -144,7 +158,7 @@ export class EpersonRegistrationService {
Object.assign(request, { Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {
return RegistrationResponseParsingService; return RegistrationResponseParsingService;
} },
}); });
this.requestService.send(request, true); this.requestService.send(request, true);
}); });
@@ -186,7 +200,7 @@ export class EpersonRegistrationService {
let operations = []; let operations = [];
if (values.length > 0 && hasValue(field) ) { if (values.length > 0 && hasValue(field) ) {
operations = [{ operations = [{
op: operator, path: `/${field}`, value: values op: operator, path: `/${field}`, value: values,
}]; }];
} }

View File

@@ -1,8 +1,19 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { fakeAsync, TestBed, tick, waitForAsync, } from '@angular/core/testing'; import {
fakeAsync,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { MockStore, provideMockStore, } from '@ngrx/store/testing'; import {
import { compare, Operation, } from 'fast-json-patch'; MockStore,
provideMockStore,
} from '@ngrx/store/testing';
import {
compare,
Operation,
} from 'fast-json-patch';
import { cold } from 'jasmine-marbles'; import { cold } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
@@ -13,12 +24,21 @@ import {
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; import {
import { EPersonMock, EPersonMock2, } from '../../shared/testing/eperson.mock'; createNoContentRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
import {
EPersonMock,
EPersonMock2,
} from '../../shared/testing/eperson.mock';
import { GroupMock } from '../../shared/testing/group-mock'; import { GroupMock } from '../../shared/testing/group-mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { createPaginatedList, createRequestEntry$, } from '../../shared/testing/utils.test'; import {
createPaginatedList,
createRequestEntry$,
} from '../../shared/testing/utils.test';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
@@ -26,13 +46,19 @@ import { CoreState } from '../core-state.model';
import { ChangeAnalyzer } from '../data/change-analyzer'; import { ChangeAnalyzer } from '../data/change-analyzer';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { FindListOptions } from '../data/find-list-options.model'; import { FindListOptions } from '../data/find-list-options.model';
import { PatchRequest, PostRequest, } from '../data/request.models'; import { RemoteData } from '../data/remote-data';
import {
PatchRequest,
PostRequest,
} from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { editEPersonSelector, EPersonDataService, } from './eperson-data.service'; import {
editEPersonSelector,
EPersonDataService,
} from './eperson-data.service';
import { EPerson } from './models/eperson.model'; import { EPerson } from './models/eperson.model';
import { RemoteData } from '../data/remote-data';
describe('EPersonDataService', () => { describe('EPersonDataService', () => {
let service: EPersonDataService; let service: EPersonDataService;

View File

@@ -1,8 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { createSelector, select, Store, } from '@ngrx/store'; import {
createSelector,
select,
Store,
} from '@ngrx/store';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { find, map, take, } from 'rxjs/operators'; import {
find,
map,
take,
} from 'rxjs/operators';
import { getEPersonEditRoute } from '../../access-control/access-control-routing-paths'; import { getEPersonEditRoute } from '../../access-control/access-control-routing-paths';
import { import {
@@ -11,27 +19,51 @@ import {
} from '../../access-control/epeople-registry/epeople-registry.actions'; } from '../../access-control/epeople-registry/epeople-registry.actions';
import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers'; import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { hasNoValue, hasValue, } from '../../shared/empty.util'; import {
hasNoValue,
hasValue,
} from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { CreateData, CreateDataImpl, } from '../data/base/create-data'; import {
import { DeleteData, DeleteDataImpl, } from '../data/base/delete-data'; CreateData,
CreateDataImpl,
} from '../data/base/create-data';
import {
DeleteData,
DeleteDataImpl,
} from '../data/base/delete-data';
import { IdentifiableDataService } from '../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { PatchData, PatchDataImpl, } from '../data/base/patch-data'; import {
import { SearchData, SearchDataImpl, } from '../data/base/search-data'; PatchData,
PatchDataImpl,
} from '../data/base/patch-data';
import {
SearchData,
SearchDataImpl,
} from '../data/base/search-data';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { FindListOptions } from '../data/find-list-options.model'; import { FindListOptions } from '../data/find-list-options.model';
import { buildPaginatedList, PaginatedList, } from '../data/paginated-list.model'; import {
buildPaginatedList,
PaginatedList,
} from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { PatchRequest, PostRequest, } from '../data/request.models'; import {
PatchRequest,
PostRequest,
} from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { RestRequestMethod } from '../data/rest-request-method'; import { RestRequestMethod } from '../data/rest-request-method';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model'; import { NoContent } from '../shared/NoContent.model';
import { getFirstSucceededRemoteData, getRemoteDataPayload, } from '../shared/operators'; import {
getFirstSucceededRemoteData,
getRemoteDataPayload,
} from '../shared/operators';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { EPerson } from './models/eperson.model'; import { EPerson } from './models/eperson.model';
@@ -375,8 +407,8 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
map((href: string) => map((href: string) =>
hasValue(metadataKey) hasValue(metadataKey)
? `${href}/${uuid}?token=${token}&override=${metadataKey}` ? `${href}/${uuid}?token=${token}&override=${metadataKey}`
: `${href}/${uuid}?token=${token}` : `${href}/${uuid}?token=${token}`,
) ),
); );
hrefObs.pipe( hrefObs.pipe(

View File

@@ -1,10 +1,10 @@
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
import { AuthRegistrationType } from '../auth/models/auth.registration-type';
import { typedObject } from '../cache/builders/build-decorators'; import { typedObject } from '../cache/builders/build-decorators';
import { MetadataValue } from './metadata.models';
import { REGISTRATION } from './registration.resource-type'; import { REGISTRATION } from './registration.resource-type';
import { ResourceType } from './resource-type'; import { ResourceType } from './resource-type';
import { UnCacheableObject } from './uncacheable-object.model'; import { UnCacheableObject } from './uncacheable-object.model';
import { MetadataValue } from './metadata.models';
import { AuthRegistrationType } from '../auth/models/auth.registration-type';
export class RegistrationDataMetadataMap { export class RegistrationDataMetadataMap {
[key: string]: RegistrationDataMetadataValue[]; [key: string]: RegistrationDataMetadataValue[];

View File

@@ -9,7 +9,7 @@ const authMethodsMap = new Map();
* @param authMethodType the type of the external login method * @param authMethodType the type of the external login method
*/ */
export function renderExternalLoginConfirmationFor( export function renderExternalLoginConfirmationFor(
authMethodType: AuthRegistrationType authMethodType: AuthRegistrationType,
) { ) {
return function decorator(objectElement: any) { return function decorator(objectElement: any) {
if (!objectElement) { if (!objectElement) {
@@ -23,7 +23,7 @@ export function renderExternalLoginConfirmationFor(
* @param authMethodType the type of the external login method * @param authMethodType the type of the external login method
*/ */
export function getExternalLoginConfirmationType( export function getExternalLoginConfirmationType(
authMethodType: AuthRegistrationType authMethodType: AuthRegistrationType,
) { ) {
return authMethodsMap.get(authMethodType); return authMethodsMap.get(authMethodType);
} }

View File

@@ -1,22 +1,20 @@
import { Component, Inject } from '@angular/core'; import { Inject } from '@angular/core';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
/** /**
* This component renders a form to complete the registration process * This component renders a form to complete the registration process
*/ */
@Component({
template: ''
})
export abstract class ExternalLoginMethodEntryComponent { export abstract class ExternalLoginMethodEntryComponent {
/** /**
* The registration data object * The registration data object
*/ */
public registratioData: Registration; public registrationData: Registration;
constructor( protected constructor(
@Inject('registrationDataProvider') protected injectedRegistrationDataObject: Registration, @Inject('registrationDataProvider') protected injectedRegistrationDataObject: Registration,
) { ) {
this.registratioData = injectedRegistrationDataObject; this.registrationData = injectedRegistrationDataObject;
} }
} }

View File

@@ -1,24 +1,40 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfirmEmailComponent } from './confirm-email.component';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateLoader, TranslateModule, TranslateService, } from '@ngx-translate/core'; import {
import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; EventEmitter,
import { ExternalLoginService } from '../../services/external-login.service'; NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import {
FormBuilder,
ReactiveFormsModule,
} from '@angular/forms';
import { By } from '@angular/platform-browser';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { AuthService } from '../../../core/auth/auth.service'; import { AuthService } from '../../../core/auth/auth.service';
import { AuthMethodType } from '../../../core/auth/models/auth.method-type'; import { AuthMethodType } from '../../../core/auth/models/auth.method-type';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { HardRedirectService } from '../../../core/services/hard-redirect.service'; import { HardRedirectService } from '../../../core/services/hard-redirect.service';
import { NativeWindowService } from '../../../core/services/window.service';
import { Registration } from '../../../core/shared/registration.model'; import { Registration } from '../../../core/shared/registration.model';
import {
MockWindow,
NativeWindowMockFactory,
} from '../../../shared/mocks/mock-native-window-ref';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { NativeWindowService } from '../../../core/services/window.service'; import { ExternalLoginService } from '../../services/external-login.service';
import { MockWindow, NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref'; import { ConfirmEmailComponent } from './confirm-email.component';
import { By } from '@angular/platform-browser';
describe('ConfirmEmailComponent', () => { describe('ConfirmEmailComponent', () => {
let component: ConfirmEmailComponent; let component: ConfirmEmailComponent;
@@ -52,7 +68,6 @@ describe('ConfirmEmailComponent', () => {
redirect: {}, redirect: {},
}); });
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ConfirmEmailComponent],
providers: [ providers: [
FormBuilder, FormBuilder,
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory }, { provide: NativeWindowService, useFactory: NativeWindowMockFactory },
@@ -65,13 +80,14 @@ describe('ConfirmEmailComponent', () => {
], ],
imports: [ imports: [
CommonModule, CommonModule,
ConfirmEmailComponent,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock, useClass: TranslateLoaderMock,
}, },
}), }),
ReactiveFormsModule ReactiveFormsModule,
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); }).compileComponents();
@@ -108,7 +124,7 @@ describe('ConfirmEmailComponent', () => {
spyOn(component as any, 'postCreateAccountFromToken'); spyOn(component as any, 'postCreateAccountFromToken');
component.submitForm(); component.submitForm();
expect( expect(
(component as any).postCreateAccountFromToken (component as any).postCreateAccountFromToken,
).toHaveBeenCalledWith('test-token', component.registrationData); ).toHaveBeenCalledWith('test-token', component.registrationData);
}); });
@@ -127,7 +143,7 @@ describe('ConfirmEmailComponent', () => {
spyOn(component as any, 'patchUpdateRegistration'); spyOn(component as any, 'patchUpdateRegistration');
component.submitForm(); component.submitForm();
expect( expect(
(component as any).postCreateAccountFromToken (component as any).postCreateAccountFromToken,
).not.toHaveBeenCalled(); ).not.toHaveBeenCalled();
expect((component as any).patchUpdateRegistration).not.toHaveBeenCalled(); expect((component as any).patchUpdateRegistration).not.toHaveBeenCalled();
}); });

View File

@@ -1,24 +1,60 @@
import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import { NgIf } from '@angular/common';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import {
import { ExternalLoginService } from '../../services/external-login.service'; ChangeDetectionStrategy,
import { TranslateService } from '@ngx-translate/core'; Component,
Inject,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import { combineLatest, Subscription, take } from 'rxjs'; import {
combineLatest,
Subscription,
take,
} from 'rxjs';
import { AuthService } from '../../../core/auth/auth.service'; import { AuthService } from '../../../core/auth/auth.service';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { HardRedirectService } from '../../../core/services/hard-redirect.service'; import { HardRedirectService } from '../../../core/services/hard-redirect.service';
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import {
NativeWindowRef,
NativeWindowService,
} from '../../../core/services/window.service';
import {
getFirstCompletedRemoteData,
getRemoteDataPayload,
} from '../../../core/shared/operators';
import { Registration } from '../../../core/shared/registration.model'; import { Registration } from '../../../core/shared/registration.model';
import { hasNoValue, hasValue } from '../../../shared/empty.util'; import {
hasNoValue,
hasValue,
} from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; import { ExternalLoginService } from '../../services/external-login.service';
@Component({ @Component({
selector: 'ds-confirm-email', selector: 'ds-confirm-email',
templateUrl: './confirm-email.component.html', templateUrl: './confirm-email.component.html',
styleUrls: ['./confirm-email.component.scss'], styleUrls: ['./confirm-email.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
TranslateModule,
NgIf,
ReactiveFormsModule,
],
}) })
export class ConfirmEmailComponent implements OnInit, OnDestroy { export class ConfirmEmailComponent implements OnInit, OnDestroy {
/** /**
@@ -55,7 +91,7 @@ export class ConfirmEmailComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.emailForm = this.formBuilder.group({ this.emailForm = this.formBuilder.group({
email: [this.registrationData.email, [Validators.required, Validators.email]] email: [this.registrationData.email, [Validators.required, Validators.email]],
}); });
} }
@@ -102,7 +138,7 @@ export class ConfirmEmailComponent implements OnInit, OnDestroy {
*/ */
private postCreateAccountFromToken( private postCreateAccountFromToken(
token: string, token: string,
registrationData: Registration registrationData: Registration,
) { ) {
// check if the netId is present // check if the netId is present
// in order to create an account, the netId is required (since the user is created without a password) // in order to create an account, the netId is required (since the user is created without a password)
@@ -131,13 +167,13 @@ export class ConfirmEmailComponent implements OnInit, OnDestroy {
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
), ),
this.externalLoginService.getExternalAuthLocation(this.registrationData.registrationType), this.externalLoginService.getExternalAuthLocation(this.registrationData.registrationType),
this.authService.getRedirectUrl().pipe(take(1)) this.authService.getRedirectUrl().pipe(take(1)),
]) ])
.subscribe(([rd, location, redirectRoute]) => { .subscribe(([rd, location, redirectRoute]) => {
if (rd.hasFailed) { if (rd.hasFailed) {
this.notificationService.error( this.notificationService.error(
this.translate.get('external-login-page.provide-email.create-account.notifications.error.header'), this.translate.get('external-login-page.provide-email.create-account.notifications.error.header'),
this.translate.get('external-login-page.provide-email.create-account.notifications.error.content') this.translate.get('external-login-page.provide-email.create-account.notifications.error.content'),
); );
} else if (rd.hasSucceeded) { } else if (rd.hasSucceeded) {
// set Redirect URL to User profile, so the user is redirected to the profile page after logging in // set Redirect URL to User profile, so the user is redirected to the profile page after logging in
@@ -145,12 +181,12 @@ export class ConfirmEmailComponent implements OnInit, OnDestroy {
const externalServerUrl = this.authService.getExternalServerRedirectUrl( const externalServerUrl = this.authService.getExternalServerRedirectUrl(
this._window.nativeWindow.origin, this._window.nativeWindow.origin,
redirectRoute, redirectRoute,
location location,
); );
// redirect to external registration type authentication url // redirect to external registration type authentication url
this.hardRedirectService.redirect(externalServerUrl); this.hardRedirectService.redirect(externalServerUrl);
} }
}) }),
); );
} }

View File

@@ -1,11 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfirmationSentComponent } from './confirmation-sent.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, EventEmitter } from '@angular/core'; import {
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; CUSTOM_ELEMENTS_SCHEMA,
EventEmitter,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { ConfirmationSentComponent } from './confirmation-sent.component';
describe('ConfirmationSentComponent', () => { describe('ConfirmationSentComponent', () => {
let component: ConfirmationSentComponent; let component: ConfirmationSentComponent;
@@ -17,27 +27,27 @@ describe('ConfirmationSentComponent', () => {
instant: (key: any) => 'Mocked Translation Text', instant: (key: any) => 'Mocked Translation Text',
onLangChange: new EventEmitter(), onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(), onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter() onDefaultLangChange: new EventEmitter(),
}; };
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ ConfirmationSentComponent ],
providers: [ providers: [
{ provide: TranslateService, useValue: translateServiceStub }, { provide: TranslateService, useValue: translateServiceStub },
], ],
imports: [ imports: [
CommonModule, CommonModule,
ConfirmationSentComponent,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock useClass: TranslateLoaderMock,
} },
}), }),
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@@ -1,9 +1,16 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
} from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@Component({ @Component({
selector: 'ds-confirmation-sent', selector: 'ds-confirmation-sent',
templateUrl: './confirmation-sent.component.html', templateUrl: './confirmation-sent.component.html',
styleUrls: ['./confirmation-sent.component.scss'], styleUrls: ['./confirmation-sent.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TranslateModule],
standalone: true,
}) })
export class ConfirmationSentComponent { } export class ConfirmationSentComponent { }

View File

@@ -1,12 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProvideEmailComponent } from './provide-email.component';
import { FormBuilder } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ExternalLoginService } from '../../services/external-login.service'; import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { ExternalLoginService } from '../../services/external-login.service';
import { ProvideEmailComponent } from './provide-email.component';
describe('ProvideEmailComponent', () => { describe('ProvideEmailComponent', () => {
let component: ProvideEmailComponent; let component: ProvideEmailComponent;
@@ -17,23 +23,23 @@ describe('ProvideEmailComponent', () => {
const externalLoginService = jasmine.createSpyObj('ExternalLoginService', ['patchUpdateRegistration']); const externalLoginService = jasmine.createSpyObj('ExternalLoginService', ['patchUpdateRegistration']);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ ProvideEmailComponent ],
providers: [ providers: [
FormBuilder, FormBuilder,
{ provide: ExternalLoginService, useValue: externalLoginService }, { provide: ExternalLoginService, useValue: externalLoginService },
], ],
imports: [ imports: [
CommonModule, CommonModule,
ProvideEmailComponent,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock useClass: TranslateLoaderMock,
} },
}), }),
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@@ -1,14 +1,33 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy } from '@angular/core'; import { NgIf } from '@angular/common';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import {
import { ExternalLoginService } from '../../services/external-login.service'; ChangeDetectionStrategy,
Component,
Input,
OnDestroy,
} from '@angular/core';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { ExternalLoginService } from '../../services/external-login.service';
@Component({ @Component({
selector: 'ds-provide-email', selector: 'ds-provide-email',
templateUrl: './provide-email.component.html', templateUrl: './provide-email.component.html',
styleUrls: ['./provide-email.component.scss'], styleUrls: ['./provide-email.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TranslateModule,
NgIf,
ReactiveFormsModule,
],
standalone: true,
}) })
export class ProvideEmailComponent implements OnDestroy { export class ProvideEmailComponent implements OnDestroy {
/** /**

View File

@@ -1,20 +1,28 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExternalLogInComponent } from './external-log-in.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { EventEmitter } from '@angular/core'; import { EventEmitter } from '@angular/core';
import { By } from '@angular/platform-browser'; import {
import { of as observableOf } from 'rxjs'; ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type'; import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
import { MetadataValue } from '../../core/shared/metadata.models'; import { MetadataValue } from '../../core/shared/metadata.models';
import { AuthService } from '../../core/auth/auth.service';
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { OrcidConfirmationComponent } from '../registration-types/orcid-confirmation/orcid-confirmation.component'; import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe'; import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
import { ConfirmEmailComponent } from '../email-confirmation/confirm-email/confirm-email.component';
import { OrcidConfirmationComponent } from '../registration-types/orcid-confirmation/orcid-confirmation.component';
import { ExternalLogInComponent } from './external-log-in.component';
describe('ExternalLogInComponent', () => { describe('ExternalLogInComponent', () => {
let component: ExternalLogInComponent; let component: ExternalLogInComponent;
@@ -37,27 +45,32 @@ describe('ExternalLogInComponent', () => {
place: -1, place: -1,
}), }),
], ],
} },
}; };
const translateServiceStub = { const translateServiceStub = {
get: () => observableOf('Info Text'), get: () => observableOf('Info Text'),
instant: (key: any) => 'Info Text', instant: (key: any) => 'Info Text',
onLangChange: new EventEmitter(), onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(), onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter() onDefaultLangChange: new EventEmitter(),
}; };
beforeEach(() => beforeEach(() =>
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, TranslateModule.forRoot({})], imports: [CommonModule, TranslateModule.forRoot({}), BrowserOnlyPipe, ExternalLogInComponent, OrcidConfirmationComponent, BrowserAnimationsModule],
declarations: [BrowserOnlyPipe, ExternalLogInComponent, OrcidConfirmationComponent],
providers: [ providers: [
{ provide: TranslateService, useValue: translateServiceStub }, { provide: TranslateService, useValue: translateServiceStub },
{ provide: AuthService, useValue: new AuthServiceMock() }, { provide: AuthService, useValue: new AuthServiceMock() },
{ provide: NgbModal, useValue: modalService }, { provide: NgbModal, useValue: modalService },
FormBuilder FormBuilder,
] ],
}).compileComponents() })
.overrideComponent(ExternalLogInComponent, {
remove: {
imports: [ConfirmEmailComponent],
},
})
.compileComponents(),
); );
beforeEach(() => { beforeEach(() => {

View File

@@ -1,19 +1,54 @@
import { ChangeDetectionStrategy, Component, Injector, Input, OnDestroy, OnInit, } from '@angular/core'; import {
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; NgComponentOutlet,
import { TranslateService } from '@ngx-translate/core'; NgIf,
} from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Injector,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {
NgbModal,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { AuthMethodType } from '../../core/auth/models/auth.method-type'; import { AuthMethodType } from '../../core/auth/models/auth.method-type';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type'; import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { hasValue, isEmpty } from '../../shared/empty.util'; import { AlertComponent } from '../../shared/alert/alert.component';
import { getExternalLoginConfirmationType } from '../decorators/external-log-in.methods-decorator';
import { AlertType } from '../../shared/alert/alert-type'; import { AlertType } from '../../shared/alert/alert-type';
import {
hasValue,
isEmpty,
} from '../../shared/empty.util';
import { ThemedLogInComponent } from '../../shared/log-in/themed-log-in.component';
import { getExternalLoginConfirmationType } from '../decorators/external-log-in.methods-decorator';
import { ConfirmEmailComponent } from '../email-confirmation/confirm-email/confirm-email.component';
import { ProvideEmailComponent } from '../email-confirmation/provide-email/provide-email.component';
@Component({ @Component({
selector: 'ds-external-log-in', selector: 'ds-external-log-in',
templateUrl: './external-log-in.component.html', templateUrl: './external-log-in.component.html',
styleUrls: ['./external-log-in.component.scss'], styleUrls: ['./external-log-in.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ProvideEmailComponent,
AlertComponent,
TranslateModule,
ConfirmEmailComponent,
ThemedLogInComponent,
NgIf,
NgComponentOutlet,
],
standalone: true,
}) })
export class ExternalLogInComponent implements OnInit, OnDestroy { export class ExternalLogInComponent implements OnInit, OnDestroy {
/** /**
@@ -108,7 +143,7 @@ export class ExternalLogInComponent implements OnInit, OnDestroy {
const authMethodUppercase = authMethod.toUpperCase(); const authMethodUppercase = authMethod.toUpperCase();
return this.translate.instant( return this.translate.instant(
'external-login.haveEmail.informationText', 'external-login.haveEmail.informationText',
{ authMethod: authMethodUppercase } { authMethod: authMethodUppercase },
); );
} }
} }

View File

@@ -1,35 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ConfirmEmailComponent } from './email-confirmation/confirm-email/confirm-email.component';
import { ConfirmationSentComponent } from './email-confirmation/confirmation-sent/confirmation-sent.component';
import { ProvideEmailComponent } from './email-confirmation/provide-email/provide-email.component';
import { ExternalLogInComponent } from './external-log-in/external-log-in.component';
import { OrcidConfirmationComponent } from './registration-types/orcid-confirmation/orcid-confirmation.component';
import { SharedModule } from '../shared/shared.module';
const COMPONENTS = [
ExternalLogInComponent,
ProvideEmailComponent,
ConfirmEmailComponent,
ConfirmationSentComponent,
];
const ENTRY_COMPONENTS = [OrcidConfirmationComponent];
@NgModule({
declarations: [...COMPONENTS, ...ENTRY_COMPONENTS],
imports: [CommonModule, SharedModule],
exports: [...COMPONENTS, ...ENTRY_COMPONENTS],
})
export class ExternalLoginModule {
/**
* NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during SSR otherwise
*/
static withEntryComponents() {
return {
ngModule: ExternalLoginModule,
providers: ENTRY_COMPONENTS.map((component) => ({ provide: component })),
};
}
}

View File

@@ -0,0 +1,53 @@
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
import {
map,
Observable,
of,
} from 'rxjs';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { RemoteData } from '../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { Registration } from '../../core/shared/registration.model';
import { hasValue } from '../../shared/empty.util';
/**
* Determines if a user can activate a route based on the registration token.
* @param route - The activated route snapshot.
* @param state - The router state snapshot.
* @param epersonRegistrationService - The eperson registration service.
* @param router - The router.
* @returns A value indicating if the user can activate the route.
*/
export const registrationTokenGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> => {
const epersonRegistrationService = inject(EpersonRegistrationService);
const router = inject(Router);
if (route.params.token) {
return epersonRegistrationService
.searchByTokenAndHandleError(route.params.token)
.pipe(
getFirstCompletedRemoteData(),
map(
(data: RemoteData<Registration>) => {
if (data.hasSucceeded && hasValue(data)) {
return true;
} else {
router.navigate(['/404']);
}
},
),
);
} else {
router.navigate(['/404']);
return of(false);
}
};

View File

@@ -1,79 +1,101 @@
import { TestBed } from '@angular/core/testing'; import {
import { RegistrationTokenGuard } from './registration-token.guard'; fakeAsync,
import { ActivatedRoute, convertToParamMap, Params, Router } from '@angular/router'; TestBed,
import { of as observableOf } from 'rxjs'; tick,
import { RouterMock } from '../../shared/mocks/router.mock'; } from '@angular/core/testing';
import { Registration } from '../../core/shared/registration.model'; import {
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; ActivatedRoute,
import { EPerson } from '../../core/eperson/models/eperson.model'; Router,
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service'; RouterStateSnapshot,
} from '@angular/router';
import {
Observable,
of as observableOf,
} from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { Registration } from '../../core/shared/registration.model';
import { RouterMock } from '../../shared/mocks/router.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { registrationTokenGuard } from './registration-token-guard';
describe('RegistrationTokenGuard', () => { describe('RegistrationTokenGuard',
let guard: RegistrationTokenGuard; () => {
const route = new RouterMock(); const route = new RouterMock();
const registrationWithGroups = Object.assign(new Registration(), const registrationWithGroups = Object.assign(new Registration(),
{ {
email: 'test@email.org', email: 'test@email.org',
token: 'test-token', token: 'test-token',
});
const epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationWithGroups),
}); });
const epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { const authService = {
searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationWithGroups) getAuthenticatedUserFromStore: () => observableOf(ePerson),
}); setRedirectUrl: () => {
const authService = { return true;
getAuthenticatedUserFromStore: () => observableOf(ePerson), },
setRedirectUrl: () => { } as any;
return true; const ePerson = Object.assign(new EPerson(), {
} id: 'test-eperson',
} as any; uuid: 'test-eperson',
const ePerson = Object.assign(new EPerson(), { });
id: 'test-eperson',
uuid: 'test-eperson' let arouteStub = {
}); snapshot: {
beforeEach(() => { params: {
const paramObject: Params = {}; token: '123456789',
paramObject.token = '1234';
TestBed.configureTestingModule({
providers: [{provide: Router, useValue: route},
{
provide: ActivatedRoute,
useValue: {
queryParamMap: observableOf(convertToParamMap(paramObject))
},
}, },
{provide: EpersonRegistrationService, useValue: epersonRegistrationService}, },
{provide: AuthService, useValue: authService} };
]
});
guard = TestBed.get(RegistrationTokenGuard);
});
it('should be created', () => { beforeEach(() => {
expect(guard).toBeTruthy(); TestBed.configureTestingModule({
}); providers: [{ provide: Router, useValue: route },
describe('based on the response of "searchByToken have', () => { {
it('can activate must return true when registration data includes groups', () => { provide: ActivatedRoute,
(guard.canActivate({ params: { token: '123456789' } } as any, {} as any) as any) useValue: arouteStub,
.subscribe( },
(canActivate) => { { provide: EpersonRegistrationService, useValue: epersonRegistrationService },
expect(canActivate).toEqual(true); { provide: AuthService, useValue: authService },
} ],
); });
}); });
it('can activate must return false when registration data includes groups', () => {
const registrationWithDifferentUsedFromLoggedInt = Object.assign(new Registration(), describe('when token provided', () => {
{ it('can activate must return true when registration data includes groups', fakeAsync(() => {
const activatedRoute = TestBed.inject(ActivatedRoute);
const result$ = TestBed.runInInjectionContext(() => {
return registrationTokenGuard(activatedRoute.snapshot, {} as RouterStateSnapshot) as Observable<boolean>;
});
let output = null;
result$.subscribe((result) => (output = result));
tick(100);
expect(output).toBeTrue();
}));
});
describe('when no token provided', () => {
it('can activate must return false when registration data includes groups', fakeAsync(() => {
const registrationWithDifferentUserFromLoggedIn = Object.assign(new Registration(), {
email: 't1@email.org', email: 't1@email.org',
token: 'test-token', token: 'test-token',
}); });
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(observableOf(registrationWithDifferentUsedFromLoggedInt)); epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(observableOf(registrationWithDifferentUserFromLoggedIn));
(guard.canActivate({ params: { token: '123456789' } } as any, {} as any) as any) let activatedRoute = TestBed.inject(ActivatedRoute);
.subscribe( activatedRoute.snapshot.params.token = null;
(canActivate) => {
expect(canActivate).toEqual(false);
}
);
});
const result$ = TestBed.runInInjectionContext(() => {
return registrationTokenGuard(activatedRoute.snapshot, {} as RouterStateSnapshot) as Observable<boolean>;
});
let output = null;
result$.subscribe((result) => (output = result));
expect(output).toBeFalse();
}));
});
}); });
});

View File

@@ -1,49 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router';
import { map, Observable, of } from 'rxjs';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { Registration } from '../../core/shared/registration.model';
import { RemoteData } from '../../core/data/remote-data';
import { hasValue } from '../../shared/empty.util';
@Injectable({
providedIn: 'root',
})
export class RegistrationTokenGuard implements CanActivate {
constructor(
private router: Router,
private epersonRegistrationService: EpersonRegistrationService
) { }
/**
* Determines if a user can activate a route based on the registration token.
* @param route - The activated route snapshot.
* @param state - The router state snapshot.
* @returns A value indicating if the user can activate the route.
*/
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
if (route.params.token) {
return this.epersonRegistrationService
.searchByTokenAndHandleError(route.params.token)
.pipe(
getFirstCompletedRemoteData(),
map(
(data: RemoteData<Registration>) => {
if (data.hasSucceeded && hasValue(data)) {
return true;
} else {
this.router.navigate(['/404']);
}
}
)
);
} else {
this.router.navigate(['/404']);
return of(false);
}
}
}

View File

@@ -1,6 +1,6 @@
import { Registration } from '../../core/shared/registration.model';
import { AuthMethodType } from '../../core/auth/models/auth.method-type'; import { AuthMethodType } from '../../core/auth/models/auth.method-type';
import { MetadataValue } from '../../core/shared/metadata.models'; import { MetadataValue } from '../../core/shared/metadata.models';
import { Registration } from '../../core/shared/registration.model';
export const mockRegistrationDataModel: Registration = Object.assign( export const mockRegistrationDataModel: Registration = Object.assign(
new Registration(), new Registration(),
@@ -41,5 +41,5 @@ export const mockRegistrationDataModel: Registration = Object.assign(
}, },
], ],
}, },
} },
); );

View File

@@ -1,6 +1,6 @@
<form class="form-login" <form class="form-login"
[formGroup]="form" novalidate> [formGroup]="form" novalidate>
<label class="font-weight-bold mb-0 text-uppercase">{{ registratioData.registrationType }}</label> <label class="font-weight-bold mb-0 text-uppercase">{{ registrationData.registrationType }}</label>
<input [attr.aria-label]="'netId' | translate" <input [attr.aria-label]="'netId' | translate"
autocomplete="off" autocomplete="off"
autofocus autofocus
@@ -23,7 +23,7 @@
formControlName="firstname" formControlName="firstname"
type="text" type="text"
[attr.data-test]="'firstname' | dsBrowserOnly"> [attr.data-test]="'firstname' | dsBrowserOnly">
<ng-container *ngIf="registratioData?.email"> <ng-container *ngIf="registrationData?.email">
<label class="font-weight-bold mb-0">{{"Email" | translate}}</label> <label class="font-weight-bold mb-0">{{"Email" | translate}}</label>
<input [attr.aria-label]="'Email' | translate" <input [attr.aria-label]="'Email' | translate"
autocomplete="off" autocomplete="off"

View File

@@ -1,14 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OrcidConfirmationComponent } from './orcid-confirmation.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import {
FormBuilder,
FormGroup,
} from '@angular/forms';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { Registration } from 'src/app/core/shared/registration.model'; import { Registration } from 'src/app/core/shared/registration.model';
import { mockRegistrationDataModel } from '../../models/registration-data.mock.model';
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
import { mockRegistrationDataModel } from '../../models/registration-data.mock.model';
import { OrcidConfirmationComponent } from './orcid-confirmation.component';
describe('OrcidConfirmationComponent', () => { describe('OrcidConfirmationComponent', () => {
let component: OrcidConfirmationComponent; let component: OrcidConfirmationComponent;
@@ -17,24 +26,22 @@ describe('OrcidConfirmationComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [
OrcidConfirmationComponent,
BrowserOnlyMockPipe,
],
providers: [ providers: [
FormBuilder, FormBuilder,
{ provide: 'registrationDataProvider', useValue: mockRegistrationDataModel }, { provide: 'registrationDataProvider', useValue: mockRegistrationDataModel },
], ],
imports: [ imports: [
OrcidConfirmationComponent,
CommonModule, CommonModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useClass: TranslateLoaderMock useClass: TranslateLoaderMock,
} },
}), }),
BrowserOnlyMockPipe,
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA],
}) })
.compileComponents(); .compileComponents();
}); });
@@ -59,7 +66,7 @@ describe('OrcidConfirmationComponent', () => {
it('should initialize the form with null email as an empty string', () => { it('should initialize the form with null email as an empty string', () => {
component.registratioData.email = null; component.registrationData.email = null;
component.ngOnInit(); component.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
const emailFormControl = component.form.get('email'); const emailFormControl = component.form.get('email');

View File

@@ -1,7 +1,20 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; import { NgIf } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import {
ChangeDetectionStrategy,
Component,
Inject,
OnInit,
} from '@angular/core';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
} from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { AuthRegistrationType } from '../../../core/auth/models/auth.registration-type'; import { AuthRegistrationType } from '../../../core/auth/models/auth.registration-type';
import { Registration } from '../../../core/shared/registration.model'; import { Registration } from '../../../core/shared/registration.model';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
import { renderExternalLoginConfirmationFor } from '../../decorators/external-log-in.methods-decorator'; import { renderExternalLoginConfirmationFor } from '../../decorators/external-log-in.methods-decorator';
import { ExternalLoginMethodEntryComponent } from '../../decorators/external-login-method-entry.component'; import { ExternalLoginMethodEntryComponent } from '../../decorators/external-login-method-entry.component';
@@ -9,7 +22,14 @@ import { ExternalLoginMethodEntryComponent } from '../../decorators/external-log
selector: 'ds-orcid-confirmation', selector: 'ds-orcid-confirmation',
templateUrl: './orcid-confirmation.component.html', templateUrl: './orcid-confirmation.component.html',
styleUrls: ['./orcid-confirmation.component.scss'], styleUrls: ['./orcid-confirmation.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
TranslateModule,
BrowserOnlyPipe,
NgIf,
],
standalone: true,
}) })
@renderExternalLoginConfirmationFor(AuthRegistrationType.Orcid) @renderExternalLoginConfirmationFor(AuthRegistrationType.Orcid)
export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponent implements OnInit { export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponent implements OnInit {
@@ -25,7 +45,7 @@ export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponen
*/ */
constructor( constructor(
@Inject('registrationDataProvider') protected injectedRegistrationDataObject: Registration, @Inject('registrationDataProvider') protected injectedRegistrationDataObject: Registration,
private formBuilder: FormBuilder private formBuilder: FormBuilder,
) { ) {
super(injectedRegistrationDataObject); super(injectedRegistrationDataObject);
} }
@@ -35,10 +55,10 @@ export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponen
*/ */
ngOnInit(): void { ngOnInit(): void {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
netId: [{ value: this.registratioData.netId, disabled: true }], netId: [{ value: this.registrationData.netId, disabled: true }],
firstname: [{ value: this.getFirstname(), disabled: true }], firstname: [{ value: this.getFirstname(), disabled: true }],
lastname: [{ value: this.getLastname(), disabled: true }], lastname: [{ value: this.getLastname(), disabled: true }],
email: [{ value: this.registratioData?.email || '', disabled: true }], // email can be null email: [{ value: this.registrationData?.email || '', disabled: true }], // email can be null
}); });
} }
@@ -47,7 +67,7 @@ export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponen
* @returns the firstname of the user * @returns the firstname of the user
*/ */
private getFirstname(): string { private getFirstname(): string {
return this.registratioData.registrationMetadata?.['eperson.firstname']?.[0]?.value || ''; return this.registrationData.registrationMetadata?.['eperson.firstname']?.[0]?.value || '';
} }
/** /**
@@ -55,6 +75,6 @@ export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponen
* @returns the lastname of the user * @returns the lastname of the user
*/ */
private getLastname(): string { private getLastname(): string {
return this.registratioData.registrationMetadata?.['eperson.lastname']?.[0]?.value || ''; return this.registrationData.registrationMetadata?.['eperson.lastname']?.[0]?.value || '';
} }
} }

View File

@@ -1,16 +1,22 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import {
ActivatedRouteSnapshot,
RouterStateSnapshot,
} from '@angular/router';
import { RegistrationDataResolver } from './registration-data.resolver';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import {
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
import { RegistrationDataResolver } from './registration-data.resolver';
describe('RegistrationDataResolver', () => { describe('RegistrationDataResolver', () => {
let resolver: RegistrationDataResolver; let resolver: RegistrationDataResolver;
let epersonRegistrationServiceSpy: jasmine.SpyObj<EpersonRegistrationService>; let epersonRegistrationServiceSpy: jasmine.SpyObj<EpersonRegistrationService>;
const registrationMock = Object.assign(new Registration(), { const registrationMock = Object.assign(new Registration(), {
email: 'test@user.com', email: 'test@user.com',
}); });
beforeEach(() => { beforeEach(() => {
@@ -19,8 +25,8 @@ describe('RegistrationDataResolver', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
RegistrationDataResolver, RegistrationDataResolver,
{ provide: EpersonRegistrationService, useValue: spy } { provide: EpersonRegistrationService, useValue: spy },
] ],
}); });
resolver = TestBed.inject(RegistrationDataResolver); resolver = TestBed.inject(RegistrationDataResolver);
epersonRegistrationServiceSpy = TestBed.inject(EpersonRegistrationService) as jasmine.SpyObj<EpersonRegistrationService>; epersonRegistrationServiceSpy = TestBed.inject(EpersonRegistrationService) as jasmine.SpyObj<EpersonRegistrationService>;

View File

@@ -1,11 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot, } from '@angular/router'; import {
ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { hasValue } from '../../shared/empty.util';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { RemoteData } from '../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { RemoteData } from '../../core/data/remote-data'; import { hasValue } from '../../shared/empty.util';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -30,7 +35,7 @@ export class RegistrationDataResolver implements Resolve<RemoteData<Registration
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Registration>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Registration>> {
const token = route.params.token; const token = route.params.token;
if (hasValue(token)) { if (hasValue(token)) {
return this.epersonRegistrationService.searchByTokenAndHandleError(token).pipe( return this.epersonRegistrationService.searchByTokenAndHandleError(token).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );
} }

View File

@@ -1,18 +1,28 @@
import { TestBed } from '@angular/core/testing';
import { ExternalLoginService } from './external-login.service';
import { TranslateService } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { provideMockStore } from '@ngrx/store/testing';
import { TranslateService } from '@ngx-translate/core';
import { getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { NoContent } from '../../core/shared/NoContent.model';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { RouterMock } from '../../shared/mocks/router.mock'; import { RouterMock } from '../../shared/mocks/router.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import {
createFailedRemoteDataObject,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { ExternalLoginService } from './external-login.service';
describe('ExternalLoginService', () => { describe('ExternalLoginService', () => {
let service: ExternalLoginService; let service: ExternalLoginService;
@@ -20,6 +30,7 @@ describe('ExternalLoginService', () => {
let router: any; let router: any;
let notificationService; let notificationService;
let translate; let translate;
let scheduler: TestScheduler;
const values = ['value1', 'value2']; const values = ['value1', 'value2'];
const field = 'field1'; const field = 'field1';
@@ -29,7 +40,7 @@ describe('ExternalLoginService', () => {
beforeEach(() => { beforeEach(() => {
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
patchUpdateRegistration: createSuccessfulRemoteDataObject$(new Registration) patchUpdateRegistration: createSuccessfulRemoteDataObject$(new Registration),
}); });
router = new RouterMock(); router = new RouterMock();
notificationService = new NotificationsServiceStub(); notificationService = new NotificationsServiceStub();
@@ -42,11 +53,12 @@ describe('ExternalLoginService', () => {
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{ provide: NotificationsService, useValue: notificationService }, { provide: NotificationsService, useValue: notificationService },
{ provide: TranslateService, useValue: translate }, { provide: TranslateService, useValue: translate },
{ provide: Store, useValue: {} }, provideMockStore(),
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA],
}); });
service = TestBed.inject(ExternalLoginService); service = TestBed.inject(ExternalLoginService);
scheduler = getTestScheduler();
}); });
it('should be created', () => { it('should be created', () => {
@@ -60,19 +72,21 @@ describe('ExternalLoginService', () => {
}); });
it('should navigate to /email-confirmation if the remote data has succeeded', () => { it('should navigate to /email-confirmation if the remote data has succeeded', () => {
const remoteData = { hasSucceeded: true } as RemoteData<Registration>; epersonRegistrationService.patchUpdateRegistration.and.returnValue(createSuccessfulRemoteDataObject$(new Registration()));
epersonRegistrationService.patchUpdateRegistration.and.returnValue(observableOf(remoteData)); scheduler.schedule(() => service.patchUpdateRegistration(values, field, registrationId, token, operation).subscribe());
service.patchUpdateRegistration(values, field, registrationId, token, operation).subscribe(() => { scheduler.flush();
expect((router as any).navigate).toHaveBeenCalledWith(['/email-confirmation']); expect((router as any).navigate).toHaveBeenCalledWith(['/email-confirmation']);
});
}); });
it('should show an error notification if the remote data has failed', () => { it('should show an error notification if the remote data has failed', fakeAsync(() => {
const remoteData = { hasFailed: true } as RemoteData<Registration>; const remoteData = createFailedRemoteDataObject<NoContent>('error message');
epersonRegistrationService.patchUpdateRegistration.and.returnValue(observableOf(remoteData)); epersonRegistrationService.patchUpdateRegistration.and.returnValue(observableOf(remoteData));
translate.get.and.returnValue(observableOf('error message')); translate.get.and.returnValue(observableOf('error message'));
service.patchUpdateRegistration(values, field, registrationId, token, operation).subscribe(() => {
expect(notificationService.error).toHaveBeenCalledWith('error message'); let result = null;
}); service.patchUpdateRegistration(values, field, registrationId, token, operation).subscribe((data) => (result = data));
}); tick(100);
expect(result).toEqual(remoteData);
expect(notificationService.error).toHaveBeenCalled();
}));
}); });

View File

@@ -1,19 +1,27 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { filter, map, Observable } from 'rxjs'; import {
select,
Store,
} from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import {
filter,
map,
Observable,
} from 'rxjs';
import { AuthMethod } from 'src/app/core/auth/models/auth.method'; import { AuthMethod } from 'src/app/core/auth/models/auth.method';
import { getAuthenticationMethods } from 'src/app/core/auth/selectors'; import { getAuthenticationMethods } from 'src/app/core/auth/selectors';
import { select, Store } from '@ngrx/store';
import { CoreState } from 'src/app/core/core-state.model'; import { CoreState } from 'src/app/core/core-state.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { NoContent } from '../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class ExternalLoginService { export class ExternalLoginService {
@@ -46,7 +54,7 @@ export class ExternalLoginService {
this.notificationService.error(this.translate.get('external-login-page.provide-email.notifications.error')); this.notificationService.error(this.translate.get('external-login-page.provide-email.notifications.error'));
} }
return rd; return rd;
}) }),
); );
} }

View File

@@ -1,4 +1,5 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { ExternalLoginEmailConfirmationPageComponent } from './external-login-email-confirmation-page.component'; import { ExternalLoginEmailConfirmationPageComponent } from './external-login-email-confirmation-page.component';
export const ROUTES: Routes = [ export const ROUTES: Routes = [

View File

@@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ExternalLoginEmailConfirmationPageComponent } from './external-login-email-confirmation-page.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: ExternalLoginEmailConfirmationPageComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ExternalLoginEmailConfirmationPageRoutingModule {}

View File

@@ -1,11 +1,15 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExternalLoginEmailConfirmationPageComponent } from './external-login-email-confirmation-page.component';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
import { import {
ConfirmationSentComponent ComponentFixture,
} from '../external-log-in/email-confirmation/confirmation-sent/confirmation-sent.component'; TestBed,
} from '@angular/core/testing';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { ConfirmationSentComponent } from '../external-log-in/email-confirmation/confirmation-sent/confirmation-sent.component';
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
import { ExternalLoginEmailConfirmationPageComponent } from './external-login-email-confirmation-page.component';
describe('ExternalLoginEmailConfirmationPageComponent', () => { describe('ExternalLoginEmailConfirmationPageComponent', () => {
let component: ExternalLoginEmailConfirmationPageComponent; let component: ExternalLoginEmailConfirmationPageComponent;
@@ -13,19 +17,18 @@ describe('ExternalLoginEmailConfirmationPageComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
ExternalLoginEmailConfirmationPageComponent, ExternalLoginEmailConfirmationPageComponent,
ConfirmationSentComponent ], ConfirmationSentComponent,
imports: [ ],
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@@ -1,8 +1,12 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ConfirmationSentComponent } from '../external-log-in/email-confirmation/confirmation-sent/confirmation-sent.component';
@Component({ @Component({
templateUrl: './external-login-email-confirmation-page.component.html', templateUrl: './external-login-email-confirmation-page.component.html',
styleUrls: ['./external-login-email-confirmation-page.component.scss'] styleUrls: ['./external-login-email-confirmation-page.component.scss'],
standalone: true,
imports: [ConfirmationSentComponent],
}) })
export class ExternalLoginEmailConfirmationPageComponent { export class ExternalLoginEmailConfirmationPageComponent {
} }

View File

@@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
ExternalLoginEmailConfirmationPageRoutingModule
} from './external-login-email-confirmation-page-routing.module';
import { ExternalLoginEmailConfirmationPageComponent } from './external-login-email-confirmation-page.component';
import { ExternalLoginModule } from '../external-log-in/external-login.module';
@NgModule({
declarations: [
ExternalLoginEmailConfirmationPageComponent
],
imports: [
CommonModule,
ExternalLoginEmailConfirmationPageRoutingModule,
ExternalLoginModule,
]
})
export class ExternalLoginEmailConfirmationPageModule { }

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ThemedExternalLoginPageComponent } from './themed-external-login-page.component';
import { RegistrationDataResolver } from '../external-log-in/resolvers/registration-data.resolver';
import { RegistrationTokenGuard } from '../external-log-in/guards/registration-token.guard';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: ThemedExternalLoginPageComponent,
canActivate: [RegistrationTokenGuard],
resolve: { registrationData: RegistrationDataResolver },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [],
})
export class ExternalLoginPageRoutingModule {}

View File

@@ -1,12 +1,19 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExternalLoginPageComponent } from './external-login-page.component';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import {
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { of } from 'rxjs';
import { Registration } from '../core/shared/registration.model'; import { Registration } from '../core/shared/registration.model';
import { ExternalLogInComponent } from '../external-log-in/external-log-in/external-log-in.component';
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
import { ExternalLoginPageComponent } from './external-login-page.component';
describe('ExternalLoginPageComponent', () => { describe('ExternalLoginPageComponent', () => {
let component: ExternalLoginPageComponent; let component: ExternalLoginPageComponent;
@@ -26,7 +33,6 @@ describe('ExternalLoginPageComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ ExternalLoginPageComponent ],
providers: [ providers: [
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
@@ -42,6 +48,7 @@ describe('ExternalLoginPageComponent', () => {
], ],
imports: [ imports: [
CommonModule, CommonModule,
ExternalLoginPageComponent,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
@@ -50,7 +57,12 @@ describe('ExternalLoginPageComponent', () => {
}), }),
], ],
}) })
.compileComponents(); .overrideComponent(ExternalLoginPageComponent, {
remove: {
imports: [ExternalLogInComponent],
},
})
.compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@@ -1,14 +1,38 @@
import { Component, OnInit } from '@angular/core'; import {
AsyncPipe,
NgIf,
} from '@angular/common';
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { hasNoValue } from '../shared/empty.util'; import { TranslateModule } from '@ngx-translate/core';
import { first, map, Observable, tap } from 'rxjs'; import {
import { Registration } from '../core/shared/registration.model'; first,
map,
Observable,
tap,
} from 'rxjs';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { Registration } from '../core/shared/registration.model';
import { ExternalLogInComponent } from '../external-log-in/external-log-in/external-log-in.component';
import { AlertComponent } from '../shared/alert/alert.component';
import { AlertType } from '../shared/alert/alert-type'; import { AlertType } from '../shared/alert/alert-type';
import { hasNoValue } from '../shared/empty.util';
@Component({ @Component({
templateUrl: './external-login-page.component.html', templateUrl: './external-login-page.component.html',
styleUrls: ['./external-login-page.component.scss'], styleUrls: ['./external-login-page.component.scss'],
imports: [
TranslateModule,
AsyncPipe,
NgIf,
ExternalLogInComponent,
AlertComponent,
],
standalone: true,
}) })
export class ExternalLoginPageComponent implements OnInit { export class ExternalLoginPageComponent implements OnInit {
/** /**
@@ -31,7 +55,7 @@ export class ExternalLoginPageComponent implements OnInit {
public hasErrors = false; public hasErrors = false;
constructor( constructor(
private arouter: ActivatedRoute private arouter: ActivatedRoute,
) { ) {
this.token = this.arouter.snapshot.params.token; this.token = this.arouter.snapshot.params.token;
this.hasErrors = hasNoValue(this.arouter.snapshot.params.token); this.hasErrors = hasNoValue(this.arouter.snapshot.params.token);
@@ -42,7 +66,7 @@ export class ExternalLoginPageComponent implements OnInit {
this.arouter.data.pipe( this.arouter.data.pipe(
first(), first(),
tap((data) => this.hasErrors = (data.registrationData as RemoteData<Registration>).hasFailed), tap((data) => this.hasErrors = (data.registrationData as RemoteData<Registration>).hasFailed),
map((data) => (data.registrationData as RemoteData<Registration>).payload) map((data) => (data.registrationData as RemoteData<Registration>).payload),
); );
} }
} }

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExternalLoginPageRoutingModule } from './external-login-page-routing.module';
import { ExternalLoginPageComponent } from './external-login-page.component';
import { ThemedExternalLoginPageComponent } from './themed-external-login-page.component';
import { SharedModule } from '../shared/shared.module';
import { ExternalLoginModule } from '../external-log-in/external-login.module';
const COMPONENTS = [
ExternalLoginPageComponent,
ThemedExternalLoginPageComponent,
];
@NgModule({
declarations: [
...COMPONENTS
],
imports: [
CommonModule,
ExternalLoginPageRoutingModule,
SharedModule,
ExternalLoginModule
]
})
export class ExternalLoginPageModule { }

View File

@@ -1,14 +1,15 @@
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { ThemedExternalLoginPageComponent } from './themed-external-login-page.component';
import { RegistrationTokenGuard } from '../external-log-in/guards/registration-token.guard'; import { registrationTokenGuard } from '../external-log-in/guards/registration-token-guard';
import { RegistrationDataResolver } from '../external-log-in/resolvers/registration-data.resolver'; import { RegistrationDataResolver } from '../external-log-in/resolvers/registration-data.resolver';
import { ThemedExternalLoginPageComponent } from './themed-external-login-page.component';
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
component: ThemedExternalLoginPageComponent, component: ThemedExternalLoginPageComponent,
canActivate: [RegistrationTokenGuard], canActivate: [registrationTokenGuard],
resolve: { registrationData: RegistrationDataResolver }, resolve: { registrationData: RegistrationDataResolver },
}, },
]; ];

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ThemedComponent } from '../shared/theme-support/themed.component'; import { ThemedComponent } from '../shared/theme-support/themed.component';
import { ExternalLoginPageComponent } from './external-login-page.component'; import { ExternalLoginPageComponent } from './external-login-page.component';
@@ -6,9 +7,11 @@ import { ExternalLoginPageComponent } from './external-login-page.component';
* Themed wrapper for ExternalLoginPageComponent * Themed wrapper for ExternalLoginPageComponent
*/ */
@Component({ @Component({
selector: 'ds-themed-external-login-page', selector: 'ds-external-login-page',
styleUrls: [], styleUrls: [],
templateUrl: './../shared/theme-support/themed.component.html' templateUrl: './../shared/theme-support/themed.component.html',
standalone: true,
imports: [ExternalLoginPageComponent],
}) })
export class ThemedExternalLoginPageComponent extends ThemedComponent<ExternalLoginPageComponent> { export class ThemedExternalLoginPageComponent extends ThemedComponent<ExternalLoginPageComponent> {
protected getComponentName(): string { protected getComponentName(): string {

View File

@@ -1,14 +1,15 @@
import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component';
import { ReviewAccountGuard } from './helpers/review-account.guard';
import { RegistrationDataResolver } from '../external-log-in/resolvers/registration-data.resolver';
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { RegistrationDataResolver } from '../external-log-in/resolvers/registration-data.resolver';
import { ReviewAccountGuard } from './helpers/review-account.guard';
import { ThemedExternalLoginReviewAccountInfoPageComponent } from './themed-external-login-review-account-info-page.component';
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
component: ExternalLoginReviewAccountInfoPageComponent, component: ThemedExternalLoginReviewAccountInfoPageComponent,
canActivate: [ReviewAccountGuard], canActivate: [ReviewAccountGuard],
resolve: { registrationData: RegistrationDataResolver }, resolve: { registrationData: RegistrationDataResolver },
}, },
] ];

View File

@@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component';
import { ReviewAccountGuard } from './helpers/review-account.guard';
import { RegistrationDataResolver } from '../external-log-in/resolvers/registration-data.resolver';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: ExternalLoginReviewAccountInfoPageComponent,
canActivate: [ReviewAccountGuard],
resolve: { registrationData: RegistrationDataResolver },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ExternalLoginReviewAccountInfoRoutingModule {}

View File

@@ -1,8 +1,15 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component';
import { mockRegistrationDataModel } from '../external-log-in/models/registration-data.mock.model'; import { mockRegistrationDataModel } from '../external-log-in/models/registration-data.mock.model';
import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component';
import { ReviewAccountInfoComponent } from './review-account-info/review-account-info.component';
describe('ExternalLoginReviewAccountInfoPageComponent', () => { describe('ExternalLoginReviewAccountInfoPageComponent', () => {
let component: ExternalLoginReviewAccountInfoPageComponent; let component: ExternalLoginReviewAccountInfoPageComponent;
@@ -11,21 +18,30 @@ describe('ExternalLoginReviewAccountInfoPageComponent', () => {
const mockActivatedRoute = { const mockActivatedRoute = {
snapshot: { snapshot: {
params: { params: {
token: '1234567890' token: '1234567890',
} },
}, },
data: of({ data: of({
registrationData: mockRegistrationDataModel registrationData: mockRegistrationDataModel,
}) }),
}; };
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ExternalLoginReviewAccountInfoPageComponent],
providers: [ providers: [
{ provide: ActivatedRoute, useValue: mockActivatedRoute } { provide: ActivatedRoute, useValue: mockActivatedRoute },
] ],
imports: [
ExternalLoginReviewAccountInfoPageComponent,
BrowserAnimationsModule,
TranslateModule.forRoot({}),
],
}) })
.overrideComponent(ExternalLoginReviewAccountInfoPageComponent, {
remove: {
imports: [ReviewAccountInfoComponent],
},
})
.compileComponents(); .compileComponents();
}); });

View File

@@ -1,14 +1,36 @@
import { Component, OnInit } from '@angular/core'; import {
import { first, map, Observable, tap } from 'rxjs'; AsyncPipe,
NgIf,
} from '@angular/common';
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { hasNoValue } from '../shared/empty.util'; import {
import { Registration } from '../core/shared/registration.model'; first,
map,
Observable,
tap,
} from 'rxjs';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { Registration } from '../core/shared/registration.model';
import { AlertComponent } from '../shared/alert/alert.component';
import { AlertType } from '../shared/alert/alert-type'; import { AlertType } from '../shared/alert/alert-type';
import { hasNoValue } from '../shared/empty.util';
import { ReviewAccountInfoComponent } from './review-account-info/review-account-info.component';
@Component({ @Component({
templateUrl: './external-login-review-account-info-page.component.html', templateUrl: './external-login-review-account-info-page.component.html',
styleUrls: ['./external-login-review-account-info-page.component.scss'] styleUrls: ['./external-login-review-account-info-page.component.scss'],
imports: [
ReviewAccountInfoComponent,
AsyncPipe,
NgIf,
AlertComponent,
],
standalone: true,
}) })
export class ExternalLoginReviewAccountInfoPageComponent implements OnInit { export class ExternalLoginReviewAccountInfoPageComponent implements OnInit {
/** /**
@@ -31,7 +53,7 @@ export class ExternalLoginReviewAccountInfoPageComponent implements OnInit {
public hasErrors = false; public hasErrors = false;
constructor( constructor(
private arouter: ActivatedRoute private arouter: ActivatedRoute,
) { ) {
this.token = this.arouter.snapshot.params.token; this.token = this.arouter.snapshot.params.token;
this.hasErrors = hasNoValue(this.arouter.snapshot.params.token); this.hasErrors = hasNoValue(this.arouter.snapshot.params.token);

View File

@@ -1,30 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExternalLoginReviewAccountInfoRoutingModule } from './external-login-review-account-info-page-routing.module';
import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component';
import { CompareValuesPipe } from './helpers/compare-values.pipe';
import {
ThemedExternalLoginReviewAccountInfoPageComponent
} from './themed-external-login-review-account-info-page.component';
import { ReviewAccountInfoComponent } from './review-account-info/review-account-info.component';
import { UiSwitchModule } from 'ngx-ui-switch';
import { SharedModule } from '../shared/shared.module';
import { ExternalLoginModule } from '../external-log-in/external-login.module';
@NgModule({
declarations: [
ExternalLoginReviewAccountInfoPageComponent,
CompareValuesPipe,
ThemedExternalLoginReviewAccountInfoPageComponent,
ReviewAccountInfoComponent
],
imports: [
CommonModule,
ExternalLoginReviewAccountInfoRoutingModule,
SharedModule,
UiSwitchModule,
ExternalLoginModule
]
})
export class ExternalLoginReviewAccountInfoModule { }

View File

@@ -1,7 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core'; import {
Pipe,
PipeTransform,
} from '@angular/core';
@Pipe({ @Pipe({
name: 'dsCompareValues' name: 'dsCompareValues',
standalone: true,
}) })
export class CompareValuesPipe implements PipeTransform { export class CompareValuesPipe implements PipeTransform {

View File

@@ -1,78 +1,121 @@
import { TestBed } from '@angular/core/testing'; import {
import { ReviewAccountGuard } from './review-account.guard'; fakeAsync,
import { ActivatedRoute, convertToParamMap, Params, Router } from '@angular/router'; TestBed,
import { of as observableOf, of } from 'rxjs'; tick,
} from '@angular/core/testing';
import {
ActivatedRoute,
convertToParamMap,
Params,
Router,
RouterStateSnapshot,
} from '@angular/router';
import {
Observable,
of as observableOf,
of,
} from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { RouterMock } from '../../shared/mocks/router.mock';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Registration } from '../../core/shared/registration.model';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type'; import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { Registration } from '../../core/shared/registration.model';
import { RouterMock } from '../../shared/mocks/router.mock';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
import { ReviewAccountGuard } from './review-account.guard';
describe('ReviewAccountGuard', () => { describe('ReviewAccountGuard', () => {
let guard: ReviewAccountGuard;
let epersonRegistrationService: any; let epersonRegistrationService: any;
let authService: any; let authService: any;
let router: any;
const route = new RouterMock(); const registrationMock = Object.assign(new Registration(), {
const registrationMock = Object.assign(new Registration(), email: 'test@email.org',
{ registrationType: AuthRegistrationType.Validation,
email: 'test@email.org', });
registrationType: AuthRegistrationType.Validation
});
beforeEach(() => { beforeEach(() => {
const paramObject: Params = {}; const paramObject: Params = { token: '1234' };
paramObject.token = '1234';
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationMock) searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationMock),
}); });
authService = { authService = {
isAuthenticated: () => observableOf(true) isAuthenticated: () => observableOf(true),
} as any; } as any;
router = new RouterMock();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [{ provide: Router, useValue: route }, providers: [
{ { provide: Router, useValue: router },
provide: ActivatedRoute, {
useValue: { provide: ActivatedRoute,
queryParamMap: observableOf(convertToParamMap(paramObject)) useValue: {
queryParamMap: observableOf(convertToParamMap(paramObject)),
snapshot: {
params: {
token: '1234',
},
},
},
}, },
}, { provide: EpersonRegistrationService, useValue: epersonRegistrationService },
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService }, { provide: AuthService, useValue: authService },
{ provide: AuthService, useValue: authService } ],
]
}); });
guard = TestBed.inject(ReviewAccountGuard);
}); });
it('should be created', () => {
expect(guard).toBeTruthy();
});
it('can activate must return true when registration type is validation', () => { it('should return true when registration type is validation', fakeAsync(() => {
(guard.canActivate({ params: { token: 'valid token' } } as any, {} as any) as any) const state = {} as RouterStateSnapshot;
.subscribe( const activatedRoute = TestBed.inject(ActivatedRoute);
(canActivate) => {
expect(canActivate).toEqual(true);
}
);
});
it('should navigate to 404 if the registration search fails', () => { const result$ = TestBed.runInInjectionContext(()=> {
return ReviewAccountGuard(activatedRoute.snapshot, state) as Observable<boolean>;
});
let output = null;
result$.subscribe((result) => (output = result));
tick(100);
expect(output).toBeTrue();
}));
it('should navigate to 404 if the registration search fails', fakeAsync(() => {
const state = {} as RouterStateSnapshot;
const activatedRoute = TestBed.inject(ActivatedRoute);
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(createFailedRemoteDataObject$()); epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(createFailedRemoteDataObject$());
(guard.canActivate({ params: { token: 'invalid-token' } } as any, {} as any) as any).subscribe((result) => {
expect(result).toBeFalse();
expect(route.navigate).toHaveBeenCalledWith(['/404']);
});
});
it('should navigate to 404 if the registration type is not validation and the user is not authenticated', () => { const result$ = TestBed.runInInjectionContext(() => {
return ReviewAccountGuard(activatedRoute.snapshot, state) as Observable<boolean>;
});
let output = null;
result$.subscribe((result) => (output = result));
tick(100);
expect(output).toBeFalse();
expect(router.navigate).toHaveBeenCalledWith(['/404']);
}));
it('should navigate to 404 if the registration type is not validation and the user is not authenticated', fakeAsync(() => {
registrationMock.registrationType = AuthRegistrationType.Orcid; registrationMock.registrationType = AuthRegistrationType.Orcid;
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(createSuccessfulRemoteDataObject$(registrationMock)); epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(createSuccessfulRemoteDataObject$(registrationMock));
spyOn(authService, 'isAuthenticated').and.returnValue(of(false)); spyOn(authService, 'isAuthenticated').and.returnValue(of(false));
(guard.canActivate({ params: { token: 'invalid-token' } } as any, {} as any) as any).subscribe((result) => { const activatedRoute = TestBed.inject(ActivatedRoute);
expect(route.navigate).toHaveBeenCalledWith(['/404']);
const result$ = TestBed.runInInjectionContext(() => {
return ReviewAccountGuard(activatedRoute.snapshot, {} as RouterStateSnapshot) as Observable<boolean>;
}); });
});
let output = null;
result$.subscribe((result) => (output = result));
tick(100);
expect(output).toBeFalse();
expect(router.navigate).toHaveBeenCalledWith(['/404']);
}));
}); });

View File

@@ -1,6 +1,18 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import {
import { catchError, mergeMap, Observable, of, tap } from 'rxjs'; ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
import {
catchError,
mergeMap,
Observable,
of,
tap,
} from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type'; import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
@@ -9,57 +21,49 @@ import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
@Injectable({ /**
providedIn: 'root', * Determines if a user can activate a route based on the registration token.z
}) * @param route - The activated route snapshot.
export class ReviewAccountGuard implements CanActivate { * @param state - The router state snapshot.
constructor( * @returns A value indicating if the user can activate the route.
private router: Router, */
private epersonRegistrationService: EpersonRegistrationService, export const ReviewAccountGuard: CanActivateFn = (
private authService: AuthService route: ActivatedRouteSnapshot,
) { } state: RouterStateSnapshot,
): Observable<boolean> => {
/** const authService = inject(AuthService);
* Determines if a user can activate a route based on the registration token. const router = inject(Router);
* @param route - The activated route snapshot. const epersonRegistrationService = inject(EpersonRegistrationService);
* @param state - The router state snapshot. if (route.params.token) {
* @returns A value indicating if the user can activate the route. return epersonRegistrationService
*/ .searchByTokenAndHandleError(route.params.token)
canActivate( .pipe(
route: ActivatedRouteSnapshot, getFirstCompletedRemoteData(),
state: RouterStateSnapshot mergeMap(
): Promise<boolean> | boolean | Observable<boolean> { (data: RemoteData<Registration>) => {
if (route.params.token) { if (data.hasSucceeded && hasValue(data.payload)) {
return this.epersonRegistrationService // is the registration type validation (account valid)
.searchByTokenAndHandleError(route.params.token) if (hasValue(data.payload.registrationType) && data.payload.registrationType.includes(AuthRegistrationType.Validation)) {
.pipe( return of(true);
getFirstCompletedRemoteData(), } else {
mergeMap( return authService.isAuthenticated();
(data: RemoteData<Registration>) => {
if (data.hasSucceeded && hasValue(data.payload)) {
// is the registration type validation (account valid)
if (hasValue(data.payload.registrationType) && data.payload.registrationType.includes(AuthRegistrationType.Validation)) {
return of(true);
} else {
return this.authService.isAuthenticated();
}
} }
return of(false);
} }
),
tap((isValid: boolean) => {
if (!isValid) {
this.router.navigate(['/404']);
}
}),
catchError(() => {
this.router.navigate(['/404']);
return of(false); return of(false);
}) },
); ),
} else { tap((isValid: boolean) => {
this.router.navigate(['/404']); if (!isValid) {
return of(false); router.navigate(['/404']);
} }
}),
catchError(() => {
router.navigate(['/404']);
return of(false);
}),
);
} else {
router.navigate(['/404']);
return of(false);
} }
} };

View File

@@ -19,7 +19,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th scope="row text-uppercase">{{ registrationData.registrationType }}</th> <th scope="row" class="text-uppercase">{{ registrationData.registrationType }}</th>
<td>{{ registrationData.netId }}</td> <td>{{ registrationData.netId }}</td>
<td> <td>
<span> <span>

View File

@@ -1,29 +1,43 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ReviewAccountInfoComponent } from './review-account-info.component';
import { TranslateLoader, TranslateModule, TranslateService, } from '@ngx-translate/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { EventEmitter } from '@angular/core';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import {
import { Observable, of, Subscription } from 'rxjs'; ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
Observable,
of,
Subscription,
} from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
import { NativeWindowService } from '../../core/services/window.service';
import { Registration } from '../../core/shared/registration.model';
import { ExternalLoginService } from '../../external-log-in/services/external-login.service';
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
import { NativeWindowMockFactory } from '../../shared/mocks/mock-native-window-ref';
import { RouterMock } from '../../shared/mocks/router.mock';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { EPersonMock } from '../../shared/testing/eperson.mock'; import { EPersonMock } from '../../shared/testing/eperson.mock';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { Router } from '@angular/router';
import { RouterMock } from '../../shared/mocks/router.mock';
import { EventEmitter } from '@angular/core';
import { CompareValuesPipe } from '../helpers/compare-values.pipe'; import { CompareValuesPipe } from '../helpers/compare-values.pipe';
import { Registration } from '../../core/shared/registration.model'; import { ReviewAccountInfoComponent } from './review-account-info.component';
import { AuthService } from '../../core/auth/auth.service';
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
import { ExternalLoginService } from '../../external-log-in/services/external-login.service';
import { NativeWindowService } from '../../core/services/window.service';
import { NativeWindowMockFactory } from '../../shared/mocks/mock-native-window-ref';
describe('ReviewAccountInfoComponent', () => { describe('ReviewAccountInfoComponent', () => {
let component: ReviewAccountInfoComponent; let component: ReviewAccountInfoComponent;
@@ -40,11 +54,11 @@ describe('ReviewAccountInfoComponent', () => {
get: () => of('test-message'), get: () => of('test-message'),
onLangChange: new EventEmitter(), onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(), onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter() onDefaultLangChange: new EventEmitter(),
}; };
const mockEPerson = EPersonMock; const mockEPerson = EPersonMock;
const modalStub = { const modalStub = {
open: () => ({ componentInstance: { response: of(true) }}), open: () => ({ componentInstance: { response: of(true) } }),
close: () => null, close: () => null,
dismiss: () => null, dismiss: () => null,
}; };
@@ -67,7 +81,7 @@ describe('ReviewAccountInfoComponent', () => {
}, },
mergeEPersonDataWithToken( mergeEPersonDataWithToken(
token: string, token: string,
metadata?: string metadata?: string,
): Observable<RemoteData<EPerson>> { ): Observable<RemoteData<EPerson>> {
return createSuccessfulRemoteDataObject$(mockEPerson); return createSuccessfulRemoteDataObject$(mockEPerson);
}, },
@@ -82,7 +96,6 @@ describe('ReviewAccountInfoComponent', () => {
}); });
authService = new AuthServiceMock(); authService = new AuthServiceMock();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ReviewAccountInfoComponent, CompareValuesPipe],
providers: [ providers: [
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory }, { provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
@@ -99,6 +112,9 @@ describe('ReviewAccountInfoComponent', () => {
], ],
imports: [ imports: [
CommonModule, CommonModule,
BrowserAnimationsModule,
ReviewAccountInfoComponent,
CompareValuesPipe,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
@@ -115,7 +131,7 @@ describe('ReviewAccountInfoComponent', () => {
componentAsAny = component; componentAsAny = component;
component.registrationData = Object.assign( component.registrationData = Object.assign(
new Registration(), new Registration(),
registrationDataMock registrationDataMock,
); );
component.registrationToken = 'test-token'; component.registrationToken = 'test-token';
fixture.detectChanges(); fixture.detectChanges();
@@ -166,7 +182,7 @@ describe('ReviewAccountInfoComponent', () => {
it('should merge EPerson data with token when overrideValue is true', fakeAsync(() => { it('should merge EPerson data with token when overrideValue is true', fakeAsync(() => {
component.dataToCompare[0].overrideValue = true; component.dataToCompare[0].overrideValue = true;
spyOn(ePersonDataServiceStub, 'mergeEPersonDataWithToken').and.returnValue( spyOn(ePersonDataServiceStub, 'mergeEPersonDataWithToken').and.returnValue(
of({ hasSucceeded: true }) of({ hasSucceeded: true }),
); );
component.mergeEPersonDataWithToken(registrationDataMock.user, registrationDataMock.registrationType); component.mergeEPersonDataWithToken(registrationDataMock.user, registrationDataMock.registrationType);
tick(); tick();

View File

@@ -1,21 +1,53 @@
import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy, OnInit, } from '@angular/core'; import {
import { EPerson } from '../../core/eperson/models/eperson.model'; NgFor,
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; NgIf,
import { combineLatest, filter, from, map, Observable, Subscription, switchMap, take, tap } from 'rxjs'; TitleCasePipe,
import { RemoteData } from '../../core/data/remote-data'; } from '@angular/common';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component'; import {
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; ChangeDetectionStrategy,
import { hasValue } from '../../shared/empty.util'; Component,
import { TranslateService } from '@ngx-translate/core'; Inject,
import { NotificationsService } from '../../shared/notifications/notifications.service'; Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Registration } from '../../core/shared/registration.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { UiSwitchModule } from 'ngx-ui-switch';
import {
combineLatest,
filter,
from,
map,
Observable,
Subscription,
switchMap,
take,
tap,
} from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type'; import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
import { RemoteData } from '../../core/data/remote-data';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
import {
NativeWindowRef,
NativeWindowService,
} from '../../core/services/window.service';
import { Registration } from '../../core/shared/registration.model';
import { ExternalLoginService } from '../../external-log-in/services/external-login.service'; import { ExternalLoginService } from '../../external-log-in/services/external-login.service';
import { NativeWindowRef, NativeWindowService } from '../../core/services/window.service'; import { AlertComponent } from '../../shared/alert/alert.component';
import { AlertType } from '../../shared/alert/alert-type'; import { AlertType } from '../../shared/alert/alert-type';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CompareValuesPipe } from '../helpers/compare-values.pipe';
export interface ReviewAccountInfoData { export interface ReviewAccountInfoData {
label: string; label: string;
@@ -29,7 +61,17 @@ export interface ReviewAccountInfoData {
selector: 'ds-review-account-info', selector: 'ds-review-account-info',
templateUrl: './review-account-info.component.html', templateUrl: './review-account-info.component.html',
styleUrls: ['./review-account-info.component.scss'], styleUrls: ['./review-account-info.component.scss'],
imports: [
CompareValuesPipe,
NgFor,
NgIf,
TitleCasePipe,
TranslateModule,
AlertComponent,
UiSwitchModule,
],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
}) })
export class ReviewAccountInfoComponent implements OnInit, OnDestroy { export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
/** /**
@@ -82,7 +124,7 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
*/ */
public onOverrideChange(value: boolean, identifier: string) { public onOverrideChange(value: boolean, identifier: string) {
this.dataToCompare.find( this.dataToCompare.find(
(data) => data.identifier === identifier (data) => data.identifier === identifier,
).overrideValue = value; ).overrideValue = value;
} }
@@ -120,11 +162,11 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
if (confirm) { if (confirm) {
this.mergeEPersonDataWithToken(userId, this.registrationData.registrationType); this.mergeEPersonDataWithToken(userId, this.registrationData.registrationType);
} }
}) }),
) ),
) ),
) )
.subscribe() .subscribe(),
); );
} else if (this.registrationData.user) { } else if (this.registrationData.user) {
this.subs.push( this.subs.push(
@@ -135,7 +177,7 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
const registrationType = this.registrationData.registrationType.split(AuthRegistrationType.Validation)[1]; const registrationType = this.registrationData.registrationType.split(AuthRegistrationType.Validation)[1];
this.mergeEPersonDataWithToken(this.registrationData.user, registrationType); this.mergeEPersonDataWithToken(this.registrationData.user, registrationType);
} }
}) }),
); );
} }
} }
@@ -154,14 +196,14 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
return this.ePersonService.mergeEPersonDataWithToken( return this.ePersonService.mergeEPersonDataWithToken(
userId, userId,
this.registrationToken, this.registrationToken,
data.identifier data.identifier,
); );
}) }),
); );
} else { } else {
override$ = this.ePersonService.mergeEPersonDataWithToken( override$ = this.ePersonService.mergeEPersonDataWithToken(
userId, userId,
this.registrationToken this.registrationToken,
); );
} }
if (this.registrationData.user && this.registrationData.registrationType.includes(AuthRegistrationType.Validation)) { if (this.registrationData.user && this.registrationData.registrationType.includes(AuthRegistrationType.Validation)) {
@@ -182,18 +224,18 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
if (response.hasSucceeded) { if (response.hasSucceeded) {
this.notificationService.success( this.notificationService.success(
this.translateService.get( this.translateService.get(
'review-account-info.merge-data.notification.success' 'review-account-info.merge-data.notification.success',
) ),
); );
this.router.navigate(['/profile']); this.router.navigate(['/profile']);
} else if (response.hasFailed) { } else if (response.hasFailed) {
this.notificationService.error( this.notificationService.error(
this.translateService.get( this.translateService.get(
'review-account-info.merge-data.notification.error' 'review-account-info.merge-data.notification.error',
) ),
); );
} }
}) }),
); );
} }
@@ -214,26 +256,26 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
if (response.hasSucceeded) { if (response.hasSucceeded) {
this.notificationService.success( this.notificationService.success(
this.translateService.get( this.translateService.get(
'review-account-info.merge-data.notification.success' 'review-account-info.merge-data.notification.success',
) ),
); );
// set Redirect URL to User profile, so the user is redirected to the profile page after logging in // set Redirect URL to User profile, so the user is redirected to the profile page after logging in
this.authService.setRedirectUrl('/profile'); this.authService.setRedirectUrl('/profile');
const externalServerUrl = this.authService.getExternalServerRedirectUrl( const externalServerUrl = this.authService.getExternalServerRedirectUrl(
this._window.nativeWindow.origin, this._window.nativeWindow.origin,
redirectRoute, redirectRoute,
location location,
); );
// redirect to external registration type authentication url // redirect to external registration type authentication url
this.hardRedirectService.redirect(externalServerUrl); this.hardRedirectService.redirect(externalServerUrl);
} else if (response.hasFailed) { } else if (response.hasFailed) {
this.notificationService.error( this.notificationService.error(
this.translateService.get( this.translateService.get(
'review-account-info.merge-data.notification.error' 'review-account-info.merge-data.notification.error',
) ),
); );
} }
}) }),
); );
} }
@@ -269,7 +311,7 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
overrideValue: false, overrideValue: false,
identifier: key, identifier: key,
}); });
} },
); );
return dataToCompare; return dataToCompare;

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ThemedComponent } from '../shared/theme-support/themed.component'; import { ThemedComponent } from '../shared/theme-support/themed.component';
import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component'; import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component';
@@ -6,9 +7,11 @@ import { ExternalLoginReviewAccountInfoPageComponent } from './external-login-re
* Themed wrapper for ExternalLoginReviewAccountInfoPageComponent * Themed wrapper for ExternalLoginReviewAccountInfoPageComponent
*/ */
@Component({ @Component({
selector: 'ds-themed-external-login-page', selector: 'ds-external-login-page',
styleUrls: [], styleUrls: [],
templateUrl: './../shared/theme-support/themed.component.html' templateUrl: './../shared/theme-support/themed.component.html',
standalone: true,
imports: [ExternalLoginReviewAccountInfoPageComponent],
}) })
export class ThemedExternalLoginReviewAccountInfoPageComponent extends ThemedComponent<ExternalLoginReviewAccountInfoPageComponent> { export class ThemedExternalLoginReviewAccountInfoPageComponent extends ThemedComponent<ExternalLoginReviewAccountInfoPageComponent> {
protected getComponentName(): string { protected getComponentName(): string {

View File

@@ -14,7 +14,7 @@ describe('registrationResolver', () => {
beforeEach(() => { beforeEach(() => {
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByTokenAndUpdateData: createSuccessfulRemoteDataObject$(registration) searchByTokenAndUpdateData: createSuccessfulRemoteDataObject$(registration),
}); });
resolver = registrationResolver; resolver = registrationResolver;
}); });

View File

@@ -1,5 +1,9 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, } from '@angular/router'; import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../core/data/eperson-registration.service';

View File

@@ -1,11 +1,18 @@
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, } from '@angular/router'; import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, } from '../shared/remote-data.utils';
import { Registration } from '../core/shared/registration.model';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { Registration } from '../core/shared/registration.model';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
} from '../shared/remote-data.utils';
import { registrationGuard } from './registration.guard'; import { registrationGuard } from './registration.guard';
describe('registrationGuard', () => { describe('registrationGuard', () => {

View File

@@ -26,7 +26,7 @@ export const registrationGuard: CanActivateFn = (
router: Router = inject(Router), router: Router = inject(Router),
): Observable<boolean> => { ): Observable<boolean> => {
const token = route.params.token; const token = route.params.token;
return epersonRegistrationService.searchByToken(token).pipe( return epersonRegistrationService.searchByTokenAndUpdateData(token).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
redirectOn4xx(router, authService), redirectOn4xx(router, authService),
map((rd) => { map((rd) => {

View File

@@ -1,10 +1,25 @@
import { AsyncPipe, NgFor, NgIf, } from '@angular/common'; import {
import { ChangeDetectionStrategy, Component, Input, OnInit, } from '@angular/core'; AsyncPipe,
import { select, Store, } from '@ngrx/store'; NgFor,
import { Observable, } from 'rxjs'; NgIf,
} from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
} from '@angular/core';
import {
select,
Store,
} from '@ngrx/store';
import uniqBy from 'lodash/uniqBy';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { AuthMethod } from '../../core/auth/models/auth.method'; import { AuthMethod } from '../../core/auth/models/auth.method';
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
import { import {
getAuthenticationError, getAuthenticationError,
getAuthenticationMethods, getAuthenticationMethods,
@@ -12,13 +27,10 @@ import {
isAuthenticationLoading, isAuthenticationLoading,
} from '../../core/auth/selectors'; } from '../../core/auth/selectors';
import { CoreState } from '../../core/core-state.model'; import { CoreState } from '../../core/core-state.model';
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
import { map } from 'rxjs/operators';
import { hasValue } from '../empty.util'; import { hasValue } from '../empty.util';
import { ThemedLoadingComponent } from '../loading/themed-loading.component'; import { ThemedLoadingComponent } from '../loading/themed-loading.component';
import { LogInContainerComponent } from './container/log-in-container.component'; import { LogInContainerComponent } from './container/log-in-container.component';
import { rendersAuthMethodType } from './methods/log-in.methods-decorator'; import { rendersAuthMethodType } from './methods/log-in.methods-decorator';
import uniqBy from 'lodash/uniqBy';
@Component({ @Component({
selector: 'ds-base-log-in', selector: 'ds-base-log-in',
@@ -78,7 +90,7 @@ export class LogInComponent implements OnInit {
.sort((method1: AuthMethod, method2: AuthMethod) => method1.position - method2.position), .sort((method1: AuthMethod, method2: AuthMethod) => method1.position - method2.position),
), ),
// ignore the ip authentication method when it's returned by the backend // ignore the ip authentication method when it's returned by the backend
map((authMethods: AuthMethod[]) => uniqBy(authMethods.filter(a => a.authMethodType !== AuthMethodType.Ip), 'authMethodType')) map((authMethods: AuthMethod[]) => uniqBy(authMethods.filter(a => a.authMethodType !== AuthMethodType.Ip), 'authMethodType')),
); );
// set loading // set loading

View File

@@ -1,6 +1,13 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync, } from '@angular/core/testing'; import {
import { ActivatedRoute, Router, } from '@angular/router'; ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { StoreModule } from '@ngrx/store'; import { StoreModule } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';

View File

@@ -1,17 +1,29 @@
import { Component, Inject, OnInit, } from '@angular/core'; import {
import { select, Store, } from '@ngrx/store'; Component,
Inject,
OnInit,
} from '@angular/core';
import {
select,
Store,
} from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { AuthMethod } from '../../../../core/auth/models/auth.method';
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
import { isEmpty } from '../../../empty.util';
import { AuthService } from '../../../../core/auth/auth.service'; import { AuthService } from '../../../../core/auth/auth.service';
import { AuthMethod } from '../../../../core/auth/models/auth.method';
import {
isAuthenticated,
isAuthenticationLoading,
} from '../../../../core/auth/selectors';
import { CoreState } from '../../../../core/core-state.model'; import { CoreState } from '../../../../core/core-state.model';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
import {
NativeWindowRef,
NativeWindowService,
} from '../../../../core/services/window.service';
import { isEmpty } from '../../../empty.util';
@Component({ @Component({
selector: 'ds-log-in-external-provider', selector: 'ds-log-in-external-provider',
@@ -91,7 +103,7 @@ export class LogInExternalProviderComponent implements OnInit {
const externalServerUrl = this.authService.getExternalServerRedirectUrl( const externalServerUrl = this.authService.getExternalServerRedirectUrl(
this._window.nativeWindow.origin, this._window.nativeWindow.origin,
redirectRoute, redirectRoute,
this.location this.location,
); );
// redirect to shibboleth/orcid/(external) authentication url // redirect to shibboleth/orcid/(external) authentication url
this.hardRedirectService.redirect(externalServerUrl); this.hardRedirectService.redirect(externalServerUrl);

View File

@@ -3,6 +3,7 @@ import {
Input, Input,
} from '@angular/core'; } from '@angular/core';
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
import { ThemedComponent } from '../theme-support/themed.component'; import { ThemedComponent } from '../theme-support/themed.component';
import { LogInComponent } from './log-in.component'; import { LogInComponent } from './log-in.component';
@@ -20,8 +21,12 @@ export class ThemedLogInComponent extends ThemedComponent<LogInComponent> {
@Input() isStandalonePage: boolean; @Input() isStandalonePage: boolean;
@Input() excludedAuthMethod: AuthMethodType;
@Input() showRegisterLink = true;
protected inAndOutputNames: (keyof LogInComponent & keyof this)[] = [ protected inAndOutputNames: (keyof LogInComponent & keyof this)[] = [
'isStandalonePage', 'isStandalonePage', 'excludedAuthMethod', 'showRegisterLink',
]; ];
protected getComponentName(): string { protected getComponentName(): string {

View File

@@ -1,5 +1,8 @@
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */ /* eslint-disable no-empty, @typescript-eslint/no-empty-function */
import { Observable, of as observableOf, } from 'rxjs'; import {
Observable,
of as observableOf,
} from 'rxjs';
export class AuthServiceMock { export class AuthServiceMock {
public checksAuthenticationToken() { public checksAuthenticationToken() {

View File

@@ -1,5 +1,9 @@
import { Observable, of as observableOf, } from 'rxjs'; import {
Observable,
of as observableOf,
} from 'rxjs';
import { RetrieveAuthMethodsAction } from '../../core/auth/auth.actions';
import { AuthMethod } from '../../core/auth/models/auth.method'; import { AuthMethod } from '../../core/auth/models/auth.method';
import { AuthMethodType } from '../../core/auth/models/auth.method-type'; import { AuthMethodType } from '../../core/auth/models/auth.method-type';
import { AuthStatus } from '../../core/auth/models/auth-status.model'; import { AuthStatus } from '../../core/auth/models/auth-status.model';
@@ -8,7 +12,6 @@ import { EPerson } from '../../core/eperson/models/eperson.model';
import { hasValue } from '../empty.util'; import { hasValue } from '../empty.util';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { EPersonMock } from './eperson.mock'; import { EPersonMock } from './eperson.mock';
import { RetrieveAuthMethodsAction } from '../../core/auth/auth.actions';
export const authMethodsMock: AuthMethod[] = [ export const authMethodsMock: AuthMethod[] = [
new AuthMethod(AuthMethodType.Password, 0), new AuthMethod(AuthMethodType.Password, 0),