Skip to content

Commit

Permalink
Merged in CST-10703 (pull request DSpace#911)
Browse files Browse the repository at this point in the history
CST-10703

Approved-by: Giuseppe Digilio
  • Loading branch information
alisaismailati authored and atarix83 committed Oct 10, 2023
2 parents b106047 + bf7f6ea commit 9080551
Show file tree
Hide file tree
Showing 86 changed files with 3,199 additions and 50 deletions.
15 changes: 15 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,21 @@ import { RedirectService } from './redirect/redirect.service';
loadChildren: () => import('./login-page/login-page.module')
.then((m) => m.LoginPageModule)
},
{
path: 'external-login/:token',
loadChildren: () => import('./external-login-page/external-login-page.module')
.then((m) => m.ExternalLoginPageModule)
},
{
path: 'review-account/:token',
loadChildren: () => import('./external-login-review-account-info-page/external-login-review-account-info-page.module')
.then((m) => m.ExternalLoginReviewAccountInfoModule)
},
{
path: 'email-confirmation',
loadChildren: () => import('./external-login-email-confirmation-page/external-login-email-confirmation-page.module')
.then((m) => m.ExternalLoginEmailConfirmationPageModule)
},
{
path: 'logout',
loadChildren: () => import('./logout-page/logout-page.module')
Expand Down
26 changes: 26 additions & 0 deletions src/app/core/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { PageInfo } from '../shared/page-info.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { MachineToken } from './models/machine-token.model';
import { NoContent } from '../shared/NoContent.model';
import { URLCombiner } from '../url-combiner/url-combiner';

export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout';
Expand Down Expand Up @@ -523,6 +524,31 @@ export class AuthService {
});
}

/**
* Returns the external server redirect URL.
* @param origin - The origin route.
* @param redirectRoute - The redirect route.
* @param location - The location.
* @returns The external server redirect URL.
*/
getExternalServerRedirectUrl(origin: string, redirectRoute: string, location: string): string {
const correctRedirectUrl = new URLCombiner(origin, redirectRoute).toString();

let externalServerUrl = location;
const myRegexp = /\?redirectUrl=(.*)/g;
const match = myRegexp.exec(location);
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;

// Check whether the current page is different from the redirect url received from rest
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
// change the redirect url with the current page url
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
externalServerUrl = location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
}

return externalServerUrl;
}

/**
* Clear redirect url
*/
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/auth/models/auth.method-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export enum AuthMethodType {
Ip = 'ip',
X509 = 'x509',
Oidc = 'oidc',
Orcid = 'orcid'
Orcid = 'orcid',
}
4 changes: 4 additions & 0 deletions src/app/core/auth/models/auth.registration-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum AuthRegistrationType {
Orcid = 'ORCID',
Validation = 'VALIDATION_',
}
2 changes: 1 addition & 1 deletion src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export const models =
WorkflowOwnerStatistics,
LoginStatistics,
Metric,
ItemRequest
ItemRequest,
];

@NgModule({
Expand Down
4 changes: 2 additions & 2 deletions src/app/core/data/eperson-registration.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('EpersonRegistrationService', () => {

describe('searchByToken', () => {
it('should return a registration corresponding to the provided token', () => {
const expected = service.searchByToken('test-token');
const expected = service.searchByTokenAndUpdateData('test-token');

expect(expected).toBeObservable(cold('(a|)', {
a: jasmine.objectContaining({
Expand All @@ -124,7 +124,7 @@ describe('EpersonRegistrationService', () => {
testScheduler.run(({ cold, expectObservable }) => {
rdbService.buildSingle.and.returnValue(cold('a', { a: rd }));

service.searchByToken('test-token');
service.searchByTokenAndUpdateData('test-token');

expect(requestService.send).toHaveBeenCalledWith(
jasmine.objectContaining({
Expand Down
65 changes: 57 additions & 8 deletions src/app/core/data/eperson-registration.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { GetRequest, PostRequest } from './request.models';
import { GetRequest, PatchRequest, PostRequest } from './request.models';
import { Observable } from 'rxjs';
import { filter, find, map } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
Expand All @@ -15,14 +15,15 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { HttpHeaders } from '@angular/common/http';
import { HttpParams } from '@angular/common/http';

import { Operation } from 'fast-json-patch';
import { NoContent } from '../shared/NoContent.model';
@Injectable({
providedIn: 'root',
})
/**
* Service that will register a new email address and request a token
*/
export class EpersonRegistrationService {
export class EpersonRegistrationService{

protected linkPath = 'registrations';
protected searchByTokenPath = '/search/findByToken?token=';
Expand All @@ -32,7 +33,6 @@ export class EpersonRegistrationService {
protected rdbService: RemoteDataBuildService,
protected halService: HALEndpointService,
) {

}

/**
Expand Down Expand Up @@ -90,10 +90,11 @@ export class EpersonRegistrationService {
}

/**
* Search a registration based on the provided token
* @param token
* Searches for a registration based on the provided token.
* @param token The token to search for.
* @returns An observable of remote data containing the registration.
*/
searchByToken(token: string): Observable<RemoteData<Registration>> {
searchByTokenAndUpdateData(token: string): Observable<RemoteData<Registration>> {
const requestId = this.requestService.generateRequestId();

const href$ = this.getTokenSearchEndpoint(token).pipe(
Expand Down Expand Up @@ -124,7 +125,13 @@ export class EpersonRegistrationService {
})
);
}
searchByTokenAndHandleError(token: string): Observable<RemoteData<Registration>> {

/**
* Searches for a registration by token and handles any errors that may occur.
* @param token The token to search for.
* @returns An observable of remote data containing the registration.
*/
searchRegistrationByToken(token: string): Observable<RemoteData<Registration>> {
const requestId = this.requestService.generateRequestId();

const href$ = this.getTokenSearchEndpoint(token).pipe(
Expand All @@ -142,4 +149,46 @@ export class EpersonRegistrationService {
});
return this.rdbService.buildSingle<Registration>(href$);
}

/**
* Patch the registration object to update the email address
* @param value provided by the user during the registration confirmation process
* @param registrationId The id of the registration object
* @param token The token of the registration object
* @param updateValue Flag to indicate if the email should be updated or added
* @returns Remote Data state of the patch request
*/
patchUpdateRegistration(values: string[], field: string, registrationId: string, token: string, operator: 'add' | 'replace'): Observable<RemoteData<NoContent>> {
const requestId = this.requestService.generateRequestId();

const href$ = this.getRegistrationEndpoint().pipe(
find((href: string) => hasValue(href)),
map((href: string) => `${href}/${registrationId}?token=${token}`),
);

href$.subscribe((href: string) => {
const operations = this.generateOperations(values, field, operator);
const patchRequest = new PatchRequest(requestId, href, operations);
this.requestService.send(patchRequest);
});

return this.rdbService.buildFromRequestUUID(requestId);
}

/**
* Custom method to generate the operations to be performed on the registration object
* @param value provided by the user during the registration confirmation process
* @param updateValue Flag to indicate if the email should be updated or added
* @returns Operations to be performed on the registration object
*/
private generateOperations(values: string[], field: string, operator: 'add' | 'replace'): Operation[] {
let operations = [];
if (values.length > 0 && hasValue(field) ) {
operations = [{
op: operator, path: `/${field}`, value: values
}];
}

return operations;
}
}
16 changes: 16 additions & 0 deletions src/app/core/eperson/eperson-data.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test';
import { CoreState } from '../core-state.model';
import { FindListOptions } from '../data/find-list-options.model';
import { RemoteData } from '../data/remote-data';

describe('EPersonDataService', () => {
let service: EPersonDataService;
Expand Down Expand Up @@ -314,6 +315,21 @@ describe('EPersonDataService', () => {
});
});

describe('mergeEPersonDataWithToken', () => {
const uuid = '1234-5678-9012-3456';
const token = 'abcd-efgh-ijkl-mnop';
const metadataKey = 'eperson.firstname';
beforeEach(() => {
spyOn(service, 'mergeEPersonDataWithToken').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock));
});

it('should merge EPerson data with token', () => {
service.mergeEPersonDataWithToken(uuid, token, metadataKey).subscribe((result: RemoteData<EPerson>) => {
expect(result.hasSucceeded).toBeTrue();
});
expect(service.mergeEPersonDataWithToken).toHaveBeenCalledWith(uuid, token, metadataKey);
});
});
});

class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
Expand Down
26 changes: 26 additions & 0 deletions src/app/core/eperson/eperson-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,32 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
return this.rdbService.buildFromRequestUUID(requestId);
}

/**
* Sends a POST request to merge registration data related to the provided registration-token,
* into the eperson related to the provided uuid
* @param uuid the user uuid
* @param token registration-token
* @param metadataKey metadata key of the metadata field that should be overriden
*/
mergeEPersonDataWithToken(uuid: string, token: string, metadataKey?: string): Observable<RemoteData<EPerson>> {
const requestId = this.requestService.generateRequestId();
const hrefObs = this.getBrowseEndpoint().pipe(
map((href: string) =>
hasValue(metadataKey)
? `${href}/${uuid}?token=${token}&override=${metadataKey}`
: `${href}/${uuid}?token=${token}`
)
);

hrefObs.pipe(
find((href: string) => hasValue(href)),
).subscribe((href: string) => {
const request = new PostRequest(requestId, href);
this.requestService.send(request);
});

return this.rdbService.buildFromRequestUUID(requestId);
}

/**
* Create a new object on the server, and store the response in the object cache
Expand Down
31 changes: 31 additions & 0 deletions src/app/core/shared/registration.model.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
// eslint-disable-next-line max-classes-per-file
import { typedObject } from '../cache/builders/build-decorators';
import { ResourceType } from './resource-type';
import { REGISTRATION } from './registration.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[];
}

export class RegistrationDataMetadataValue extends MetadataValue {
overrides?: string;
}
@typedObject
export class Registration implements UnCacheableObject {
static type = REGISTRATION;

/**
* The unique identifier of this registration data
*/
id: string;

/**
* The object type
*/
Expand All @@ -29,8 +44,24 @@ export class Registration implements UnCacheableObject {
* The token linked to the registration
*/
groupNames: string[];

/**
* The token linked to the registration
*/
groups: string[];

/**
* The registration type (e.g. orcid, shibboleth, etc.)
*/
registrationType?: AuthRegistrationType;

/**
* The netId of the user (e.g. for ORCID - <:orcid>)
*/
netId?: string;

/**
* The metadata involved during the registration process
*/
registrationMetadata?: RegistrationDataMetadataMap;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';

/**
* Map to store the external login confirmation component for the given auth method type
*/
const authMethodsMap = new Map();
/**
* Decorator to register the external login confirmation component for the given auth method type
* @param authMethodType the type of the external login method
*/
export function renderExternalLoginConfirmationFor(
authMethodType: AuthRegistrationType
) {
return function decorator(objectElement: any) {
if (!objectElement) {
return;
}
authMethodsMap.set(authMethodType, objectElement);
};
}
/**
* Get the external login confirmation component for the given auth method type
* @param authMethodType the type of the external login method
*/
export function getExternalLoginConfirmationType(
authMethodType: AuthRegistrationType
) {
return authMethodsMap.get(authMethodType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Component, 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;

constructor(
@Inject('registrationDataProvider') protected injectedRegistrationDataObject: Registration,
) {
this.registratioData = injectedRegistrationDataObject;
}
}
Loading

0 comments on commit 9080551

Please sign in to comment.