[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 {
@@ -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 { authBlockingGuard } from './core/auth/auth-blocking.guard';
import { authenticatedGuard } from './core/auth/authenticated.guard';
import {
groupAdministratorGuard
} 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 { groupAdministratorGuard } 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 { endUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.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 { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
import {
ThemedPageInternalServerErrorComponent
} from './page-internal-server-error/themed-page-internal-server-error.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
import { provideSubmissionState } from './submission/provide-submission-state';
@@ -261,17 +259,17 @@ export const APP_ROUTES: Route[] = [
},
{
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',
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',
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 },
],

View File

@@ -1,22 +1,36 @@
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 { 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 { 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 { 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 { AuthStatus } from './models/auth-status.model';
import { ShortLivedToken } from './models/short-lived-token.model';
import { MachineToken } from './models/machine-token.model';
import { NoContent } from '../shared/NoContent.model';
import { sendRequest } from '../shared/request.operators';
import { ShortLivedToken } from './models/short-lived-token.model';
/**
* Abstract service to send authentication requests
@@ -144,7 +158,7 @@ export abstract class AuthRequestService {
map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
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 { Inject, Injectable, Optional, } from '@angular/core';
import {
Inject,
Injectable,
Optional,
} from '@angular/core';
import { Router } from '@angular/router';
import { select, Store, } from '@ngrx/store';
import {
select,
Store,
} from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { CookieAttributes } from 'js-cookie';
import { Observable, of as observableOf, } from 'rxjs';
import { filter, map, startWith, switchMap, take, } from 'rxjs/operators';
import {
Observable,
of as observableOf,
} from 'rxjs';
import {
filter,
map,
startWith,
switchMap,
take,
} from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { REQUEST, RESPONSE, } from '../../../express.tokens';
import {
REQUEST,
RESPONSE,
} from '../../../express.tokens';
import { AppState } from '../../app.reducer';
import {
hasNoValue,
@@ -22,7 +41,10 @@ import {
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
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 { HttpOptions } from '../dspace-rest/dspace-rest.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 { RouteService } from '../services/route.service';
import {
getAuthenticatedUserId,
getAuthenticationToken,
getExternalAuthCookieStatus,
getRedirectUrl,
isAuthenticated,
isAuthenticatedLoaded,
isIdle,
isTokenRefreshing
} from './selectors';
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../shared/operators';
NativeWindowRef,
NativeWindowService,
} from '../services/window.service';
import { NoContent } from '../shared/NoContent.model';
import {
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
} from '../shared/operators';
import { PageInfo } from '../shared/page-info.model';
import { URLCombiner } from '../url-combiner/url-combiner';
import {
CheckAuthenticationTokenAction,
RefreshTokenAction,
@@ -55,11 +76,21 @@ import {
import { AuthRequestService } from './auth-request.service';
import { AuthMethod } from './models/auth.method';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo, TOKENITEM, } from './models/auth-token-info.model';
import { NoContent } from '../shared/NoContent.model';
import { URLCombiner } from '../url-combiner/url-combiner';
import {
AuthTokenInfo,
TOKENITEM,
} from './models/auth-token-info.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 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 { 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 { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
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 { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { GetRequest, PatchRequest, PostRequest } from './request.models';
import { Operation } from 'fast-json-patch';
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 { HttpOptions } from '../dspace-rest/dspace-rest.service';
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 { Registration } from '../shared/registration.model';
import { ResponseParsingService } from './parsing.service';
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
import { RemoteData } from './remote-data';
import { Operation } from 'fast-json-patch';
import { NoContent } from '../shared/NoContent.model';
import {
GetRequest,
PatchRequest,
PostRequest,
} from './request.models';
import { RequestService } from './request.service';
@Injectable({
providedIn: 'root',
@@ -116,10 +130,10 @@ export class EpersonRegistrationService {
map((rd) => {
if (rd.hasSucceeded && hasValue(rd.payload)) {
return Object.assign(rd, { payload: Object.assign(new Registration(), {
email: rd.payload.email,
token: token,
user: rd.payload.user,
}) });
email: rd.payload.email,
token: token,
user: rd.payload.user,
}) });
} else {
return rd;
}
@@ -144,7 +158,7 @@ export class EpersonRegistrationService {
Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return RegistrationResponseParsingService;
}
},
});
this.requestService.send(request, true);
});
@@ -186,7 +200,7 @@ export class EpersonRegistrationService {
let operations = [];
if (values.length > 0 && hasValue(field) ) {
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 { fakeAsync, TestBed, tick, waitForAsync, } from '@angular/core/testing';
import {
fakeAsync,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { MockStore, provideMockStore, } from '@ngrx/store/testing';
import { compare, Operation, } from 'fast-json-patch';
import {
MockStore,
provideMockStore,
} from '@ngrx/store/testing';
import {
compare,
Operation,
} from 'fast-json-patch';
import { cold } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
@@ -13,12 +24,21 @@ import {
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils';
import { EPersonMock, EPersonMock2, } from '../../shared/testing/eperson.mock';
import {
createNoContentRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
import {
EPersonMock,
EPersonMock2,
} from '../../shared/testing/eperson.mock';
import { GroupMock } from '../../shared/testing/group-mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-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 { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service';
@@ -26,13 +46,19 @@ import { CoreState } from '../core-state.model';
import { ChangeAnalyzer } from '../data/change-analyzer';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
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 { HALEndpointService } from '../shared/hal-endpoint.service';
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 { RemoteData } from '../data/remote-data';
describe('EPersonDataService', () => {
let service: EPersonDataService;

View File

@@ -1,8 +1,16 @@
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 { 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 {
@@ -11,27 +19,51 @@ import {
} from '../../access-control/epeople-registry/epeople-registry.actions';
import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers';
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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CreateData, CreateDataImpl, } from '../data/base/create-data';
import { DeleteData, DeleteDataImpl, } from '../data/base/delete-data';
import {
CreateData,
CreateDataImpl,
} from '../data/base/create-data';
import {
DeleteData,
DeleteDataImpl,
} from '../data/base/delete-data';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { PatchData, PatchDataImpl, } from '../data/base/patch-data';
import { SearchData, SearchDataImpl, } from '../data/base/search-data';
import {
PatchData,
PatchDataImpl,
} from '../data/base/patch-data';
import {
SearchData,
SearchDataImpl,
} from '../data/base/search-data';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
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 { PatchRequest, PostRequest, } from '../data/request.models';
import {
PatchRequest,
PostRequest,
} from '../data/request.models';
import { RequestService } from '../data/request.service';
import { RestRequestMethod } from '../data/rest-request-method';
import { HALEndpointService } from '../shared/hal-endpoint.service';
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 { EPerson } from './models/eperson.model';
@@ -375,8 +407,8 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
map((href: string) =>
hasValue(metadataKey)
? `${href}/${uuid}?token=${token}&override=${metadataKey}`
: `${href}/${uuid}?token=${token}`
)
: `${href}/${uuid}?token=${token}`,
),
);
hrefObs.pipe(

View File

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

View File

@@ -9,7 +9,7 @@ const authMethodsMap = new Map();
* @param authMethodType the type of the external login method
*/
export function renderExternalLoginConfirmationFor(
authMethodType: AuthRegistrationType
authMethodType: AuthRegistrationType,
) {
return function decorator(objectElement: any) {
if (!objectElement) {
@@ -23,7 +23,7 @@ export function renderExternalLoginConfirmationFor(
* @param authMethodType the type of the external login method
*/
export function getExternalLoginConfirmationType(
authMethodType: AuthRegistrationType
authMethodType: AuthRegistrationType,
) {
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';
/**
* This component renders a form to complete the registration process
*/
@Component({
template: ''
})
export abstract class ExternalLoginMethodEntryComponent {
/**
* The registration data object
*/
public registratioData: Registration;
public registrationData: Registration;
constructor(
protected constructor(
@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 { TranslateLoader, TranslateModule, TranslateService, } from '@ngx-translate/core';
import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { ExternalLoginService } from '../../services/external-login.service';
import {
EventEmitter,
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 { AuthService } from '../../../core/auth/auth.service';
import { AuthMethodType } from '../../../core/auth/models/auth.method-type';
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 { NativeWindowService } from '../../../core/services/window.service';
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 { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { NativeWindowService } from '../../../core/services/window.service';
import { MockWindow, NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
import { By } from '@angular/platform-browser';
import { ExternalLoginService } from '../../services/external-login.service';
import { ConfirmEmailComponent } from './confirm-email.component';
describe('ConfirmEmailComponent', () => {
let component: ConfirmEmailComponent;
@@ -52,7 +68,6 @@ describe('ConfirmEmailComponent', () => {
redirect: {},
});
await TestBed.configureTestingModule({
declarations: [ConfirmEmailComponent],
providers: [
FormBuilder,
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
@@ -65,13 +80,14 @@ describe('ConfirmEmailComponent', () => {
],
imports: [
CommonModule,
ConfirmEmailComponent,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
ReactiveFormsModule
ReactiveFormsModule,
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
@@ -108,7 +124,7 @@ describe('ConfirmEmailComponent', () => {
spyOn(component as any, 'postCreateAccountFromToken');
component.submitForm();
expect(
(component as any).postCreateAccountFromToken
(component as any).postCreateAccountFromToken,
).toHaveBeenCalledWith('test-token', component.registrationData);
});
@@ -127,7 +143,7 @@ describe('ConfirmEmailComponent', () => {
spyOn(component as any, 'patchUpdateRegistration');
component.submitForm();
expect(
(component as any).postCreateAccountFromToken
(component as any).postCreateAccountFromToken,
).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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ExternalLoginService } from '../../services/external-login.service';
import { TranslateService } from '@ngx-translate/core';
import { NgIf } from '@angular/common';
import {
ChangeDetectionStrategy,
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 { combineLatest, Subscription, take } from 'rxjs';
import {
combineLatest,
Subscription,
take,
} from 'rxjs';
import { AuthService } from '../../../core/auth/auth.service';
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 { 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 { hasNoValue, hasValue } from '../../../shared/empty.util';
import {
hasNoValue,
hasValue,
} from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
import { ExternalLoginService } from '../../services/external-login.service';
@Component({
selector: 'ds-confirm-email',
templateUrl: './confirm-email.component.html',
styleUrls: ['./confirm-email.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
TranslateModule,
NgIf,
ReactiveFormsModule,
],
})
export class ConfirmEmailComponent implements OnInit, OnDestroy {
/**
@@ -55,7 +91,7 @@ export class ConfirmEmailComponent implements OnInit, OnDestroy {
ngOnInit() {
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(
token: string,
registrationData: Registration
registrationData: Registration,
) {
// check if the netId is present
// 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(),
),
this.externalLoginService.getExternalAuthLocation(this.registrationData.registrationType),
this.authService.getRedirectUrl().pipe(take(1))
this.authService.getRedirectUrl().pipe(take(1)),
])
.subscribe(([rd, location, redirectRoute]) => {
if (rd.hasFailed) {
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.content')
this.translate.get('external-login-page.provide-email.create-account.notifications.error.content'),
);
} else if (rd.hasSucceeded) {
// 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(
this._window.nativeWindow.origin,
redirectRoute,
location
location,
);
// redirect to external registration type authentication url
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 { CUSTOM_ELEMENTS_SCHEMA, EventEmitter } from '@angular/core';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import {
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 { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { ConfirmationSentComponent } from './confirmation-sent.component';
describe('ConfirmationSentComponent', () => {
let component: ConfirmationSentComponent;
@@ -17,27 +27,27 @@ describe('ConfirmationSentComponent', () => {
instant: (key: any) => 'Mocked Translation Text',
onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter()
onDefaultLangChange: new EventEmitter(),
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ConfirmationSentComponent ],
providers: [
{ provide: TranslateService, useValue: translateServiceStub },
],
imports: [
],
imports: [
CommonModule,
ConfirmationSentComponent,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
useClass: TranslateLoaderMock,
},
}),
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
.compileComponents();
});
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({
selector: 'ds-confirmation-sent',
templateUrl: './confirmation-sent.component.html',
styleUrls: ['./confirmation-sent.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TranslateModule],
standalone: true,
})
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 { TranslateLoader, TranslateModule } from '@ngx-translate/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 { ExternalLoginService } from '../../services/external-login.service';
import { ProvideEmailComponent } from './provide-email.component';
describe('ProvideEmailComponent', () => {
let component: ProvideEmailComponent;
@@ -17,23 +23,23 @@ describe('ProvideEmailComponent', () => {
const externalLoginService = jasmine.createSpyObj('ExternalLoginService', ['patchUpdateRegistration']);
await TestBed.configureTestingModule({
declarations: [ ProvideEmailComponent ],
providers: [
FormBuilder,
{ provide: ExternalLoginService, useValue: externalLoginService },
],
imports: [
CommonModule,
ProvideEmailComponent,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
useClass: TranslateLoaderMock,
},
}),
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
.compileComponents();
});
beforeEach(() => {

View File

@@ -1,14 +1,33 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ExternalLoginService } from '../../services/external-login.service';
import { NgIf } from '@angular/common';
import {
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 { hasValue } from '../../../shared/empty.util';
import { ExternalLoginService } from '../../services/external-login.service';
@Component({
selector: 'ds-provide-email',
templateUrl: './provide-email.component.html',
styleUrls: ['./provide-email.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TranslateModule,
NgIf,
ReactiveFormsModule,
],
standalone: true,
})
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 { EventEmitter } from '@angular/core';
import { By } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
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 {
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 { 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 { 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 { 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', () => {
let component: ExternalLogInComponent;
@@ -37,27 +45,32 @@ describe('ExternalLogInComponent', () => {
place: -1,
}),
],
}
},
};
const translateServiceStub = {
get: () => observableOf('Info Text'),
instant: (key: any) => 'Info Text',
onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter()
onDefaultLangChange: new EventEmitter(),
};
beforeEach(() =>
TestBed.configureTestingModule({
imports: [CommonModule, TranslateModule.forRoot({})],
declarations: [BrowserOnlyPipe, ExternalLogInComponent, OrcidConfirmationComponent],
imports: [CommonModule, TranslateModule.forRoot({}), BrowserOnlyPipe, ExternalLogInComponent, OrcidConfirmationComponent, BrowserAnimationsModule],
providers: [
{ provide: TranslateService, useValue: translateServiceStub },
{ provide: AuthService, useValue: new AuthServiceMock() },
{ provide: NgbModal, useValue: modalService },
FormBuilder
]
}).compileComponents()
FormBuilder,
],
})
.overrideComponent(ExternalLogInComponent, {
remove: {
imports: [ConfirmEmailComponent],
},
})
.compileComponents(),
);
beforeEach(() => {

View File

@@ -1,19 +1,54 @@
import { ChangeDetectionStrategy, Component, Injector, Input, OnDestroy, OnInit, } from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import {
NgComponentOutlet,
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 { AuthMethodType } from '../../core/auth/models/auth.method-type';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
import { Registration } from '../../core/shared/registration.model';
import { hasValue, isEmpty } from '../../shared/empty.util';
import { getExternalLoginConfirmationType } from '../decorators/external-log-in.methods-decorator';
import { AlertComponent } from '../../shared/alert/alert.component';
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({
selector: 'ds-external-log-in',
templateUrl: './external-log-in.component.html',
styleUrls: ['./external-log-in.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ProvideEmailComponent,
AlertComponent,
TranslateModule,
ConfirmEmailComponent,
ThemedLogInComponent,
NgIf,
NgComponentOutlet,
],
standalone: true,
})
export class ExternalLogInComponent implements OnInit, OnDestroy {
/**
@@ -108,7 +143,7 @@ export class ExternalLogInComponent implements OnInit, OnDestroy {
const authMethodUppercase = authMethod.toUpperCase();
return this.translate.instant(
'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 { RegistrationTokenGuard } from './registration-token.guard';
import { ActivatedRoute, convertToParamMap, Params, Router } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { RouterMock } from '../../shared/mocks/router.mock';
import { Registration } from '../../core/shared/registration.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import {
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import {
ActivatedRoute,
Router,
RouterStateSnapshot,
} from '@angular/router';
import {
Observable,
of as observableOf,
} from 'rxjs';
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', () => {
let guard: RegistrationTokenGuard;
const route = new RouterMock();
const registrationWithGroups = Object.assign(new Registration(),
{
email: 'test@email.org',
token: 'test-token',
describe('RegistrationTokenGuard',
() => {
const route = new RouterMock();
const registrationWithGroups = Object.assign(new Registration(),
{
email: 'test@email.org',
token: 'test-token',
});
const epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationWithGroups),
});
const epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationWithGroups)
});
const authService = {
getAuthenticatedUserFromStore: () => observableOf(ePerson),
setRedirectUrl: () => {
return true;
}
} as any;
const ePerson = Object.assign(new EPerson(), {
id: 'test-eperson',
uuid: 'test-eperson'
});
beforeEach(() => {
const paramObject: Params = {};
paramObject.token = '1234';
TestBed.configureTestingModule({
providers: [{provide: Router, useValue: route},
{
provide: ActivatedRoute,
useValue: {
queryParamMap: observableOf(convertToParamMap(paramObject))
},
const authService = {
getAuthenticatedUserFromStore: () => observableOf(ePerson),
setRedirectUrl: () => {
return true;
},
} as any;
const ePerson = Object.assign(new EPerson(), {
id: 'test-eperson',
uuid: 'test-eperson',
});
let arouteStub = {
snapshot: {
params: {
token: '123456789',
},
{provide: EpersonRegistrationService, useValue: epersonRegistrationService},
{provide: AuthService, useValue: authService}
]
});
guard = TestBed.get(RegistrationTokenGuard);
});
},
};
it('should be created', () => {
expect(guard).toBeTruthy();
});
describe('based on the response of "searchByToken have', () => {
it('can activate must return true when registration data includes groups', () => {
(guard.canActivate({ params: { token: '123456789' } } as any, {} as any) as any)
.subscribe(
(canActivate) => {
expect(canActivate).toEqual(true);
}
);
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{ provide: Router, useValue: route },
{
provide: ActivatedRoute,
useValue: arouteStub,
},
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
{ 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',
token: 'test-token',
});
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(observableOf(registrationWithDifferentUsedFromLoggedInt));
(guard.canActivate({ params: { token: '123456789' } } as any, {} as any) as any)
.subscribe(
(canActivate) => {
expect(canActivate).toEqual(false);
}
);
});
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(observableOf(registrationWithDifferentUserFromLoggedIn));
let activatedRoute = TestBed.inject(ActivatedRoute);
activatedRoute.snapshot.params.token = null;
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 { MetadataValue } from '../../core/shared/metadata.models';
import { Registration } from '../../core/shared/registration.model';
export const mockRegistrationDataModel: Registration = Object.assign(
new Registration(),
@@ -41,5 +41,5 @@ export const mockRegistrationDataModel: Registration = Object.assign(
},
],
},
}
},
);

View File

@@ -1,6 +1,6 @@
<form class="form-login"
[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"
autocomplete="off"
autofocus
@@ -23,7 +23,7 @@
formControlName="firstname"
type="text"
[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>
<input [attr.aria-label]="'Email' | translate"
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 { 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 { 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 { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
import { mockRegistrationDataModel } from '../../models/registration-data.mock.model';
import { OrcidConfirmationComponent } from './orcid-confirmation.component';
describe('OrcidConfirmationComponent', () => {
let component: OrcidConfirmationComponent;
@@ -17,24 +26,22 @@ describe('OrcidConfirmationComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
OrcidConfirmationComponent,
BrowserOnlyMockPipe,
],
providers: [
FormBuilder,
{ provide: 'registrationDataProvider', useValue: mockRegistrationDataModel },
],
imports: [
OrcidConfirmationComponent,
CommonModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
useClass: TranslateLoaderMock,
},
}),
BrowserOnlyMockPipe,
],
schemas: [NO_ERRORS_SCHEMA]
schemas: [NO_ERRORS_SCHEMA],
})
.compileComponents();
});
@@ -59,7 +66,7 @@ describe('OrcidConfirmationComponent', () => {
it('should initialize the form with null email as an empty string', () => {
component.registratioData.email = null;
component.registrationData.email = null;
component.ngOnInit();
fixture.detectChanges();
const emailFormControl = component.form.get('email');

View File

@@ -1,7 +1,20 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { NgIf } from '@angular/common';
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 { Registration } from '../../../core/shared/registration.model';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
import { renderExternalLoginConfirmationFor } from '../../decorators/external-log-in.methods-decorator';
import { ExternalLoginMethodEntryComponent } from '../../decorators/external-login-method-entry.component';
@@ -9,7 +22,14 @@ import { ExternalLoginMethodEntryComponent } from '../../decorators/external-log
selector: 'ds-orcid-confirmation',
templateUrl: './orcid-confirmation.component.html',
styleUrls: ['./orcid-confirmation.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
TranslateModule,
BrowserOnlyPipe,
NgIf,
],
standalone: true,
})
@renderExternalLoginConfirmationFor(AuthRegistrationType.Orcid)
export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponent implements OnInit {
@@ -25,7 +45,7 @@ export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponen
*/
constructor(
@Inject('registrationDataProvider') protected injectedRegistrationDataObject: Registration,
private formBuilder: FormBuilder
private formBuilder: FormBuilder,
) {
super(injectedRegistrationDataObject);
}
@@ -35,10 +55,10 @@ export class OrcidConfirmationComponent extends ExternalLoginMethodEntryComponen
*/
ngOnInit(): void {
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 }],
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
*/
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
*/
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 {
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 { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Registration } from '../../core/shared/registration.model';
import {
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
import { RegistrationDataResolver } from './registration-data.resolver';
describe('RegistrationDataResolver', () => {
let resolver: RegistrationDataResolver;
let epersonRegistrationServiceSpy: jasmine.SpyObj<EpersonRegistrationService>;
const registrationMock = Object.assign(new Registration(), {
email: 'test@user.com',
email: 'test@user.com',
});
beforeEach(() => {
@@ -19,8 +25,8 @@ describe('RegistrationDataResolver', () => {
TestBed.configureTestingModule({
providers: [
RegistrationDataResolver,
{ provide: EpersonRegistrationService, useValue: spy }
]
{ provide: EpersonRegistrationService, useValue: spy },
],
});
resolver = TestBed.inject(RegistrationDataResolver);
epersonRegistrationServiceSpy = TestBed.inject(EpersonRegistrationService) as jasmine.SpyObj<EpersonRegistrationService>;

View File

@@ -1,11 +1,16 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot, } from '@angular/router';
import {
ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot,
} from '@angular/router';
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 { Registration } from '../../core/shared/registration.model';
import { RemoteData } from '../../core/data/remote-data';
import { EpersonRegistrationService } from '../../core/data/eperson-registration.service';
import { hasValue } from '../../shared/empty.util';
@Injectable({
providedIn: 'root',
@@ -30,7 +35,7 @@ export class RegistrationDataResolver implements Resolve<RemoteData<Registration
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Registration>> {
const token = route.params.token;
if (hasValue(token)) {
return this.epersonRegistrationService.searchByTokenAndHandleError(token).pipe(
return this.epersonRegistrationService.searchByTokenAndHandleError(token).pipe(
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 {
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
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 { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteData } from '../../core/data/remote-data';
import { NoContent } from '../../core/shared/NoContent.model';
import { Registration } from '../../core/shared/registration.model';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
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', () => {
let service: ExternalLoginService;
@@ -20,6 +30,7 @@ describe('ExternalLoginService', () => {
let router: any;
let notificationService;
let translate;
let scheduler: TestScheduler;
const values = ['value1', 'value2'];
const field = 'field1';
@@ -29,7 +40,7 @@ describe('ExternalLoginService', () => {
beforeEach(() => {
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
patchUpdateRegistration: createSuccessfulRemoteDataObject$(new Registration)
patchUpdateRegistration: createSuccessfulRemoteDataObject$(new Registration),
});
router = new RouterMock();
notificationService = new NotificationsServiceStub();
@@ -42,11 +53,12 @@ describe('ExternalLoginService', () => {
{ provide: Router, useValue: router },
{ provide: NotificationsService, useValue: notificationService },
{ provide: TranslateService, useValue: translate },
{ provide: Store, useValue: {} },
provideMockStore(),
],
schemas: [NO_ERRORS_SCHEMA]
schemas: [NO_ERRORS_SCHEMA],
});
service = TestBed.inject(ExternalLoginService);
scheduler = getTestScheduler();
});
it('should be created', () => {
@@ -60,19 +72,21 @@ describe('ExternalLoginService', () => {
});
it('should navigate to /email-confirmation if the remote data has succeeded', () => {
const remoteData = { hasSucceeded: true } as RemoteData<Registration>;
epersonRegistrationService.patchUpdateRegistration.and.returnValue(observableOf(remoteData));
service.patchUpdateRegistration(values, field, registrationId, token, operation).subscribe(() => {
expect((router as any).navigate).toHaveBeenCalledWith(['/email-confirmation']);
});
epersonRegistrationService.patchUpdateRegistration.and.returnValue(createSuccessfulRemoteDataObject$(new Registration()));
scheduler.schedule(() => service.patchUpdateRegistration(values, field, registrationId, token, operation).subscribe());
scheduler.flush();
expect((router as any).navigate).toHaveBeenCalledWith(['/email-confirmation']);
});
it('should show an error notification if the remote data has failed', () => {
const remoteData = { hasFailed: true } as RemoteData<Registration>;
it('should show an error notification if the remote data has failed', fakeAsync(() => {
const remoteData = createFailedRemoteDataObject<NoContent>('error message');
epersonRegistrationService.patchUpdateRegistration.and.returnValue(observableOf(remoteData));
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 { Router } from '@angular/router';
import { filter, map, Observable } from 'rxjs';
import {
select,
Store,
} from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import {
filter,
map,
Observable,
} from 'rxjs';
import { AuthMethod } from 'src/app/core/auth/models/auth.method';
import { getAuthenticationMethods } from 'src/app/core/auth/selectors';
import { select, Store } from '@ngrx/store';
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 { 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';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ExternalLoginService {
@@ -46,7 +54,7 @@ export class ExternalLoginService {
this.notificationService.error(this.translate.get('external-login-page.provide-email.notifications.error'));
}
return rd;
})
}),
);
}

View File

@@ -1,4 +1,5 @@
import { Routes } from '@angular/router';
import { ExternalLoginEmailConfirmationPageComponent } from './external-login-email-confirmation-page.component';
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 {
ConfirmationSentComponent
} from '../external-log-in/email-confirmation/confirmation-sent/confirmation-sent.component';
ComponentFixture,
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', () => {
let component: ExternalLoginEmailConfirmationPageComponent;
@@ -13,19 +17,18 @@ describe('ExternalLoginEmailConfirmationPageComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
ExternalLoginEmailConfirmationPageComponent,
ConfirmationSentComponent ],
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
]
ConfirmationSentComponent,
],
})
.compileComponents();
.compileComponents();
});
beforeEach(() => {

View File

@@ -1,8 +1,12 @@
import { Component } from '@angular/core';
import { ConfirmationSentComponent } from '../external-log-in/email-confirmation/confirmation-sent/confirmation-sent.component';
@Component({
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 {
}

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 { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
import {
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 { 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', () => {
let component: ExternalLoginPageComponent;
@@ -26,7 +33,6 @@ describe('ExternalLoginPageComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ExternalLoginPageComponent ],
providers: [
{
provide: ActivatedRoute,
@@ -42,6 +48,7 @@ describe('ExternalLoginPageComponent', () => {
],
imports: [
CommonModule,
ExternalLoginPageComponent,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@@ -50,7 +57,12 @@ describe('ExternalLoginPageComponent', () => {
}),
],
})
.compileComponents();
.overrideComponent(ExternalLoginPageComponent, {
remove: {
imports: [ExternalLogInComponent],
},
})
.compileComponents();
});
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 { hasNoValue } from '../shared/empty.util';
import { first, map, Observable, tap } from 'rxjs';
import { Registration } from '../core/shared/registration.model';
import { TranslateModule } from '@ngx-translate/core';
import {
first,
map,
Observable,
tap,
} from 'rxjs';
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 { hasNoValue } from '../shared/empty.util';
@Component({
templateUrl: './external-login-page.component.html',
styleUrls: ['./external-login-page.component.scss'],
imports: [
TranslateModule,
AsyncPipe,
NgIf,
ExternalLogInComponent,
AlertComponent,
],
standalone: true,
})
export class ExternalLoginPageComponent implements OnInit {
/**
@@ -31,7 +55,7 @@ export class ExternalLoginPageComponent implements OnInit {
public hasErrors = false;
constructor(
private arouter: ActivatedRoute
private arouter: ActivatedRoute,
) {
this.token = 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(
first(),
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 { 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 { ThemedExternalLoginPageComponent } from './themed-external-login-page.component';
export const ROUTES: Route[] = [
{
path: '',
pathMatch: 'full',
component: ThemedExternalLoginPageComponent,
canActivate: [RegistrationTokenGuard],
canActivate: [registrationTokenGuard],
resolve: { registrationData: RegistrationDataResolver },
},
];

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../shared/theme-support/themed.component';
import { ExternalLoginPageComponent } from './external-login-page.component';
@@ -6,9 +7,11 @@ import { ExternalLoginPageComponent } from './external-login-page.component';
* Themed wrapper for ExternalLoginPageComponent
*/
@Component({
selector: 'ds-themed-external-login-page',
selector: 'ds-external-login-page',
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> {
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 { 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[] = [
{
path: '',
pathMatch: 'full',
component: ExternalLoginReviewAccountInfoPageComponent,
component: ThemedExternalLoginReviewAccountInfoPageComponent,
canActivate: [ReviewAccountGuard],
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 { TranslateModule } from '@ngx-translate/core';
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 { ExternalLoginReviewAccountInfoPageComponent } from './external-login-review-account-info-page.component';
import { ReviewAccountInfoComponent } from './review-account-info/review-account-info.component';
describe('ExternalLoginReviewAccountInfoPageComponent', () => {
let component: ExternalLoginReviewAccountInfoPageComponent;
@@ -11,21 +18,30 @@ describe('ExternalLoginReviewAccountInfoPageComponent', () => {
const mockActivatedRoute = {
snapshot: {
params: {
token: '1234567890'
}
token: '1234567890',
},
},
data: of({
registrationData: mockRegistrationDataModel
})
registrationData: mockRegistrationDataModel,
}),
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ExternalLoginReviewAccountInfoPageComponent],
providers: [
{ provide: ActivatedRoute, useValue: mockActivatedRoute }
]
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
],
imports: [
ExternalLoginReviewAccountInfoPageComponent,
BrowserAnimationsModule,
TranslateModule.forRoot({}),
],
})
.overrideComponent(ExternalLoginReviewAccountInfoPageComponent, {
remove: {
imports: [ReviewAccountInfoComponent],
},
})
.compileComponents();
});

View File

@@ -1,14 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { first, map, Observable, tap } from 'rxjs';
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { hasNoValue } from '../shared/empty.util';
import { Registration } from '../core/shared/registration.model';
import {
first,
map,
Observable,
tap,
} from 'rxjs';
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 { hasNoValue } from '../shared/empty.util';
import { ReviewAccountInfoComponent } from './review-account-info/review-account-info.component';
@Component({
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 {
/**
@@ -31,7 +53,7 @@ export class ExternalLoginReviewAccountInfoPageComponent implements OnInit {
public hasErrors = false;
constructor(
private arouter: ActivatedRoute
private arouter: ActivatedRoute,
) {
this.token = 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({
name: 'dsCompareValues'
name: 'dsCompareValues',
standalone: true,
})
export class CompareValuesPipe implements PipeTransform {

View File

@@ -1,78 +1,121 @@
import { TestBed } from '@angular/core/testing';
import { ReviewAccountGuard } from './review-account.guard';
import { ActivatedRoute, convertToParamMap, Params, Router } from '@angular/router';
import { of as observableOf, of } from 'rxjs';
import {
fakeAsync,
TestBed,
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 { 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 { 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', () => {
let guard: ReviewAccountGuard;
let epersonRegistrationService: any;
let authService: any;
let router: any;
const route = new RouterMock();
const registrationMock = Object.assign(new Registration(),
{
email: 'test@email.org',
registrationType: AuthRegistrationType.Validation
});
const registrationMock = Object.assign(new Registration(), {
email: 'test@email.org',
registrationType: AuthRegistrationType.Validation,
});
beforeEach(() => {
const paramObject: Params = {};
paramObject.token = '1234';
const paramObject: Params = { token: '1234' };
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationMock)
searchByTokenAndHandleError: createSuccessfulRemoteDataObject$(registrationMock),
});
authService = {
isAuthenticated: () => observableOf(true)
isAuthenticated: () => observableOf(true),
} as any;
router = new RouterMock();
TestBed.configureTestingModule({
providers: [{ provide: Router, useValue: route },
{
provide: ActivatedRoute,
useValue: {
queryParamMap: observableOf(convertToParamMap(paramObject))
providers: [
{ provide: Router, useValue: router },
{
provide: ActivatedRoute,
useValue: {
queryParamMap: observableOf(convertToParamMap(paramObject)),
snapshot: {
params: {
token: '1234',
},
},
},
},
},
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
{ provide: AuthService, useValue: authService }
]
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
{ 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', () => {
(guard.canActivate({ params: { token: 'valid token' } } as any, {} as any) as any)
.subscribe(
(canActivate) => {
expect(canActivate).toEqual(true);
}
);
});
it('should return true when registration type is validation', fakeAsync(() => {
const state = {} as RouterStateSnapshot;
const activatedRoute = TestBed.inject(ActivatedRoute);
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$());
(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;
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(createSuccessfulRemoteDataObject$(registrationMock));
spyOn(authService, 'isAuthenticated').and.returnValue(of(false));
(guard.canActivate({ params: { token: 'invalid-token' } } as any, {} as any) as any).subscribe((result) => {
expect(route.navigate).toHaveBeenCalledWith(['/404']);
const activatedRoute = TestBed.inject(ActivatedRoute);
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 { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router';
import { catchError, mergeMap, Observable, of, tap } from 'rxjs';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
import {
catchError,
mergeMap,
Observable,
of,
tap,
} from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
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 { hasValue } from '../../shared/empty.util';
@Injectable({
providedIn: 'root',
})
export class ReviewAccountGuard implements CanActivate {
constructor(
private router: Router,
private epersonRegistrationService: EpersonRegistrationService,
private authService: AuthService
) { }
/**
* 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
): Promise<boolean> | boolean | Observable<boolean> {
if (route.params.token) {
return this.epersonRegistrationService
.searchByTokenAndHandleError(route.params.token)
.pipe(
getFirstCompletedRemoteData(),
mergeMap(
(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();
}
/**
* Determines if a user can activate a route based on the registration token.z
* @param route - The activated route snapshot.
* @param state - The router state snapshot.
* @returns A value indicating if the user can activate the route.
*/
export const ReviewAccountGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> => {
const authService = inject(AuthService);
const router = inject(Router);
const epersonRegistrationService = inject(EpersonRegistrationService);
if (route.params.token) {
return epersonRegistrationService
.searchByTokenAndHandleError(route.params.token)
.pipe(
getFirstCompletedRemoteData(),
mergeMap(
(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 authService.isAuthenticated();
}
return of(false);
}
),
tap((isValid: boolean) => {
if (!isValid) {
this.router.navigate(['/404']);
}
}),
catchError(() => {
this.router.navigate(['/404']);
return of(false);
})
);
} else {
this.router.navigate(['/404']);
return of(false);
}
},
),
tap((isValid: boolean) => {
if (!isValid) {
router.navigate(['/404']);
}
}),
catchError(() => {
router.navigate(['/404']);
return of(false);
}),
);
} else {
router.navigate(['/404']);
return of(false);
}
}
};

View File

@@ -19,7 +19,7 @@
</thead>
<tbody>
<tr>
<th scope="row text-uppercase">{{ registrationData.registrationType }}</th>
<th scope="row" class="text-uppercase">{{ registrationData.registrationType }}</th>
<td>{{ registrationData.netId }}</td>
<td>
<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 { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { Observable, of, Subscription } from 'rxjs';
import { EventEmitter } from '@angular/core';
import {
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 { EPersonDataService } from '../../core/eperson/eperson-data.service';
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 { 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 { Router } from '@angular/router';
import { RouterMock } from '../../shared/mocks/router.mock';
import { EventEmitter } from '@angular/core';
import { CompareValuesPipe } from '../helpers/compare-values.pipe';
import { Registration } from '../../core/shared/registration.model';
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';
import { ReviewAccountInfoComponent } from './review-account-info.component';
describe('ReviewAccountInfoComponent', () => {
let component: ReviewAccountInfoComponent;
@@ -40,11 +54,11 @@ describe('ReviewAccountInfoComponent', () => {
get: () => of('test-message'),
onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter()
onDefaultLangChange: new EventEmitter(),
};
const mockEPerson = EPersonMock;
const modalStub = {
open: () => ({ componentInstance: { response: of(true) }}),
open: () => ({ componentInstance: { response: of(true) } }),
close: () => null,
dismiss: () => null,
};
@@ -67,7 +81,7 @@ describe('ReviewAccountInfoComponent', () => {
},
mergeEPersonDataWithToken(
token: string,
metadata?: string
metadata?: string,
): Observable<RemoteData<EPerson>> {
return createSuccessfulRemoteDataObject$(mockEPerson);
},
@@ -82,7 +96,6 @@ describe('ReviewAccountInfoComponent', () => {
});
authService = new AuthServiceMock();
await TestBed.configureTestingModule({
declarations: [ReviewAccountInfoComponent, CompareValuesPipe],
providers: [
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
@@ -99,6 +112,9 @@ describe('ReviewAccountInfoComponent', () => {
],
imports: [
CommonModule,
BrowserAnimationsModule,
ReviewAccountInfoComponent,
CompareValuesPipe,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@@ -115,7 +131,7 @@ describe('ReviewAccountInfoComponent', () => {
componentAsAny = component;
component.registrationData = Object.assign(
new Registration(),
registrationDataMock
registrationDataMock,
);
component.registrationToken = 'test-token';
fixture.detectChanges();
@@ -166,7 +182,7 @@ describe('ReviewAccountInfoComponent', () => {
it('should merge EPerson data with token when overrideValue is true', fakeAsync(() => {
component.dataToCompare[0].overrideValue = true;
spyOn(ePersonDataServiceStub, 'mergeEPersonDataWithToken').and.returnValue(
of({ hasSucceeded: true })
of({ hasSucceeded: true }),
);
component.mergeEPersonDataWithToken(registrationDataMock.user, registrationDataMock.registrationType);
tick();

View File

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

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../shared/theme-support/themed.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
*/
@Component({
selector: 'ds-themed-external-login-page',
selector: 'ds-external-login-page',
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> {
protected getComponentName(): string {

View File

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

View File

@@ -1,5 +1,9 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, } from '@angular/router';
import {
ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
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 { 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 { Registration } from '../core/shared/registration.model';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
} from '../shared/remote-data.utils';
import { registrationGuard } from './registration.guard';
describe('registrationGuard', () => {

View File

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

View File

@@ -1,10 +1,25 @@
import { AsyncPipe, NgFor, NgIf, } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, OnInit, } from '@angular/core';
import { select, Store, } from '@ngrx/store';
import { Observable, } from 'rxjs';
import {
AsyncPipe,
NgFor,
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 { AuthMethod } from '../../core/auth/models/auth.method';
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
import {
getAuthenticationError,
getAuthenticationMethods,
@@ -12,13 +27,10 @@ import {
isAuthenticationLoading,
} from '../../core/auth/selectors';
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 { ThemedLoadingComponent } from '../loading/themed-loading.component';
import { LogInContainerComponent } from './container/log-in-container.component';
import { rendersAuthMethodType } from './methods/log-in.methods-decorator';
import uniqBy from 'lodash/uniqBy';
@Component({
selector: 'ds-base-log-in',
@@ -78,7 +90,7 @@ export class LogInComponent implements OnInit {
.sort((method1: AuthMethod, method2: AuthMethod) => method1.position - method2.position),
),
// 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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
/* 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 {
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 { AuthMethodType } from '../../core/auth/models/auth.method-type';
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 { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { EPersonMock } from './eperson.mock';
import { RetrieveAuthMethodsAction } from '../../core/auth/auth.actions';
export const authMethodsMock: AuthMethod[] = [
new AuthMethod(AuthMethodType.Password, 0),