[CST-15074][#3355] PR review

This commit is contained in:
Vincenzo Mecca
2025-03-07 21:22:10 +01:00
parent 77b027a6c6
commit f56b6856dd
13 changed files with 145 additions and 122 deletions

View File

@@ -8,15 +8,11 @@ import {
tap, tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { import { isNotEmpty } from '../../shared/empty.util';
isNotEmpty,
isNotEmptyOperator,
} from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { import {
DeleteRequest,
GetRequest, GetRequest,
PostRequest, PostRequest,
} from '../data/request.models'; } from '../data/request.models';
@@ -24,12 +20,9 @@ import { RequestService } from '../data/request.service';
import { RestRequest } from '../data/rest-request.model'; import { RestRequest } from '../data/rest-request.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
import { sendRequest } from '../shared/request.operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { MachineToken } from './models/machine-token.model';
import { ShortLivedToken } from './models/short-lived-token.model'; import { ShortLivedToken } from './models/short-lived-token.model';
/** /**
@@ -38,7 +31,6 @@ import { ShortLivedToken } from './models/short-lived-token.model';
export abstract class AuthRequestService { export abstract class AuthRequestService {
protected linkName = 'authn'; protected linkName = 'authn';
protected shortlivedtokensEndpoint = 'shortlivedtokens'; protected shortlivedtokensEndpoint = 'shortlivedtokens';
protected machinetokenEndpoint = 'machinetokens';
constructor(protected halService: HALEndpointService, constructor(protected halService: HALEndpointService,
protected requestService: RequestService, protected requestService: RequestService,
@@ -148,31 +140,4 @@ export abstract class AuthRequestService {
); );
} }
/**
* Send a post request to create a machine token
*/
public postToMachineTokenEndpoint(): Observable<RemoteData<MachineToken>> {
return this.halService.getEndpoint(this.linkName).pipe(
isNotEmptyOperator(),
distinctUntilChanged(),
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)),
);
}
/**
* Send a delete request to destroy a machine token
*/
public deleteToMachineTokenEndpoint(): Observable<RemoteData<NoContent>> {
return this.halService.getEndpoint(this.linkName).pipe(
isNotEmptyOperator(),
distinctUntilChanged(),
map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()),
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
sendRequest(this.requestService),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<MachineToken>(request.uuid)),
);
}
} }

View File

@@ -57,7 +57,6 @@ import {
NativeWindowRef, NativeWindowRef,
NativeWindowService, NativeWindowService,
} from '../services/window.service'; } from '../services/window.service';
import { NoContent } from '../shared/NoContent.model';
import { import {
getAllSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
@@ -80,7 +79,6 @@ import {
AuthTokenInfo, AuthTokenInfo,
TOKENITEM, TOKENITEM,
} from './models/auth-token-info.model'; } from './models/auth-token-info.model';
import { MachineToken } from './models/machine-token.model';
import { import {
getAuthenticatedUserId, getAuthenticatedUserId,
getAuthenticationToken, getAuthenticationToken,
@@ -692,18 +690,4 @@ export class AuthService {
} }
} }
/**
* Create a new machine token for the current user
*/
public createMachineToken(): Observable<RemoteData<MachineToken>> {
return this.authRequestService.postToMachineTokenEndpoint();
}
/**
* Delete the machine token for the current user
*/
public deleteMachineToken(): Observable<RemoteData<NoContent>> {
return this.authRequestService.deleteToMachineTokenEndpoint();
}
} }

View File

@@ -1,40 +0,0 @@
import {
autoserialize,
autoserializeAs,
deserialize,
} from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { MACHINE_TOKEN } from './machine-token.resource-type';
/**
* A machine token that can be used to authenticate a rest request
*/
@typedObject
export class MachineToken implements CacheableObject {
static type = MACHINE_TOKEN;
/**
* The type for this MachineToken
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* The value for this MachineToken
*/
@autoserializeAs('token')
value: string;
/**
* The {@link HALLink}s for this MachineToken
*/
@deserialize
_links: {
self: HALLink;
};
}

View File

@@ -1,9 +0,0 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for MachineToken
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const MACHINE_TOKEN = new ResourceType('machinetoken');

View File

@@ -10,6 +10,7 @@
formControlName="email" formControlName="email"
placeholder="profile.email@example.com" placeholder="profile.email@example.com"
class="form-control form-control-lg position-relative" class="form-control form-control-lg position-relative"
[attr.aria-label]="'external-login.confirmation.email-label' | translate"
/> />
<div <div
*ngIf=" *ngIf="

View File

@@ -9,6 +9,7 @@
id="email" id="email"
formControlName="email" formControlName="email"
class="form-control form-control-lg position-relative" class="form-control form-control-lg position-relative"
[attr.aria-label]="'external-login.confirmation.email' | translate"
/> />
<div <div

View File

@@ -18,7 +18,7 @@
</ng-template> </ng-template>
</div> </div>
<div class="col-1 align-items-center d-flex justify-content-center"> <div class="col-1 align-items-center d-flex justify-content-center">
<h4 class="mt-2">or</h4> <h4 class="mt-2">{{ 'external-login.component.or' | translate }}</h4>
</div> </div>
<div class="col-4 align-items-center d-flex justify-content-start"> <div class="col-4 align-items-center d-flex justify-content-start">
<button class="btn block btn-lg btn-primary" (click)="openLoginModal(loginModal)"> <button class="btn block btn-lg btn-primary" (click)="openLoginModal(loginModal)">

View File

@@ -18,7 +18,11 @@ import { EpersonRegistrationService } from '../../core/data/eperson-registration
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { RouterMock } from '../../shared/mocks/router.mock'; import { RouterMock } from '../../shared/mocks/router.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import {
createFailedRemoteDataObject$,
createNoContentRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
import { registrationTokenGuard } from './registration-token-guard'; import { registrationTokenGuard } from './registration-token-guard';
describe('RegistrationTokenGuard', describe('RegistrationTokenGuard',
@@ -98,4 +102,32 @@ describe('RegistrationTokenGuard',
expect(output).toBeFalse(); expect(output).toBeFalse();
})); }));
}); });
describe('when invalid token provided', () => {
it('can activate must navigate through 404 when no content received', fakeAsync(() => {
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(createNoContentRemoteDataObject$());
let activatedRoute = TestBed.inject(ActivatedRoute);
activatedRoute.snapshot.params.token = 'invalid-token';
const result$ = TestBed.runInInjectionContext(() => {
return registrationTokenGuard(activatedRoute.snapshot, {} as RouterStateSnapshot) as Observable<boolean>;
});
result$.subscribe((_) => { });
expect(route.navigate).toHaveBeenCalledWith(['/404']);
}));
it('can activate must navigate through 404 when no failed response received', fakeAsync(() => {
epersonRegistrationService.searchByTokenAndHandleError.and.returnValue(createFailedRemoteDataObject$('invalid token', 404));
let activatedRoute = TestBed.inject(ActivatedRoute);
activatedRoute.snapshot.params.token = 'error-invalid-token';
const result$ = TestBed.runInInjectionContext(() => {
return registrationTokenGuard(activatedRoute.snapshot, {} as RouterStateSnapshot) as Observable<boolean>;
});
result$.subscribe((_) => { });
expect(route.navigate).toHaveBeenCalledWith(['/404']);
}));
});
}); });

View File

@@ -1,31 +1,31 @@
<form class="form-login" <form class="form-login"
[formGroup]="form" novalidate> [formGroup]="form" novalidate>
<label class="font-weight-bold mb-0 text-uppercase">{{ registrationData.registrationType }}</label> <label class="font-weight-bold mb-0 text-uppercase">{{ registrationData.registrationType }}</label>
<input [attr.aria-label]="'netId' | translate" <input [attr.aria-label]="'external-login-page.orcid-confirmation.netid.label' | translate"
autocomplete="off" autocomplete="off"
autofocus autofocus
class="form-control form-control-lg position-relative mb-2" class="form-control form-control-lg position-relative mb-2"
formControlName="netId" formControlName="netId"
placeholder="xxxx-xxxx-xxxx-xxxx" [placeholder]="'external-login-page.orcid-confirmation.netid.placeholder' | translate"
type="text" type="text"
[attr.data-test]="'netId' | dsBrowserOnly"> [attr.data-test]="'netId' | dsBrowserOnly">
<label class="font-weight-bold mb-0">{{"Last name" | translate}}</label> <label class="font-weight-bold mb-0">{{"external-login-page.orcid-confirmation.lastname" | translate}}</label>
<input [attr.aria-label]="'Last name' | translate" <input [attr.aria-label]="'external-login-page.orcid-confirmation.lastname.label' | translate"
autocomplete="off" autocomplete="off"
class="form-control form-control-lg position-relative mb-2" class="form-control form-control-lg position-relative mb-2"
formControlName="lastname" formControlName="lastname"
type="text" type="text"
[attr.data-test]="'lastname' | dsBrowserOnly"> [attr.data-test]="'lastname' | dsBrowserOnly">
<label class="font-weight-bold mb-0">{{"First name" | translate}}</label> <label class="font-weight-bold mb-0">{{"external-login-page.orcid-confirmation.firstname" | translate}}</label>
<input [attr.aria-label]="'First name' | translate" <input [attr.aria-label]="'external-login-page.orcid-confirmation.firstname.label' | translate"
autocomplete="off" autocomplete="off"
class="form-control form-control-lg position-relative mb-2" class="form-control form-control-lg position-relative mb-2"
formControlName="firstname" formControlName="firstname"
type="text" type="text"
[attr.data-test]="'firstname' | dsBrowserOnly"> [attr.data-test]="'firstname' | dsBrowserOnly">
<ng-container *ngIf="registrationData?.email"> <ng-container *ngIf="registrationData?.email">
<label class="font-weight-bold mb-0">{{"Email" | translate}}</label> <label class="font-weight-bold mb-0">{{"external-login-page.orcid-confirmation.email" | translate}}</label>
<input [attr.aria-label]="'Email' | translate" <input [attr.aria-label]="'external-login-page.orcid-confirmation.email.label' | translate"
autocomplete="off" autocomplete="off"
class="form-control form-control-lg position-relative" class="form-control form-control-lg position-relative"
formControlName="email" formControlName="email"

View File

@@ -23,7 +23,7 @@
<td>{{ registrationData.netId }}</td> <td>{{ registrationData.netId }}</td>
<td> <td>
<span> <span>
{{ notApplicableText }} {{ 'external-login-validation.review-account-info.table.row.not-applicable' }}
</span> </span>
</td> </td>
<td></td> <td></td>

View File

@@ -1,10 +1,34 @@
import { NgFor, NgIf, TitleCasePipe, } from '@angular/common'; import {
import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy, OnInit, } from '@angular/core'; NgFor,
NgIf,
TitleCasePipe,
} from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule, TranslateService, } from '@ngx-translate/core'; import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { UiSwitchModule } from 'ngx-ui-switch'; import { UiSwitchModule } from 'ngx-ui-switch';
import { combineLatest, filter, from, map, Observable, Subscription, switchMap, take, tap, } from 'rxjs'; import {
combineLatest,
filter,
from,
map,
Observable,
Subscription,
switchMap,
take,
tap,
} from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type'; import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
@@ -12,7 +36,10 @@ import { RemoteData } from '../../core/data/remote-data';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { HardRedirectService } from '../../core/services/hard-redirect.service'; import { HardRedirectService } from '../../core/services/hard-redirect.service';
import { NativeWindowRef, NativeWindowService, } from '../../core/services/window.service'; import {
NativeWindowRef,
NativeWindowService,
} from '../../core/services/window.service';
import { Registration } from '../../core/shared/registration.model'; import { Registration } from '../../core/shared/registration.model';
import { ExternalLoginService } from '../../external-log-in/services/external-login.service'; import { ExternalLoginService } from '../../external-log-in/services/external-login.service';
import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertComponent } from '../../shared/alert/alert.component';
@@ -65,10 +92,6 @@ export class ReviewAccountInfoComponent implements OnInit, OnDestroy {
*/ */
@Input() registrationData: Registration; @Input() registrationData: Registration;
/**
* Text to display when the value is not applicable
*/
notApplicableText = 'N/A';
/** /**
* List of data to compare * List of data to compare
*/ */

View File

@@ -6796,6 +6796,8 @@
"dynamic-form-array.sortable-list.label": "Sortable list", "dynamic-form-array.sortable-list.label": "Sortable list",
"external-login.component.or": "or",
"external-login.confirmation.header": "Information needed to complete the login process", "external-login.confirmation.header": "Information needed to complete the login process",
"external-login.noEmail.informationText": "The information received from {{authMethod}} are not sufficient to complete the login process. Please provide the missing information below, or login via a different method to associate your {{authMethod}} to an existing account.", "external-login.noEmail.informationText": "The information received from {{authMethod}} are not sufficient to complete the login process. Please provide the missing information below, or login via a different method to associate your {{authMethod}} to an existing account.",
@@ -6806,6 +6808,8 @@
"external-login.confirmation.email-required": "Email is required.", "external-login.confirmation.email-required": "Email is required.",
"external-login.confirmation.email-label": "User Email",
"external-login.confirmation.email-invalid": "Invalid email format.", "external-login.confirmation.email-invalid": "Invalid email format.",
"external-login.confirm.button.label": "Confirm this email", "external-login.confirm.button.label": "Confirm this email",
@@ -6830,6 +6834,8 @@
"external-login-validation.review-account-info.table.header.action": "Override", "external-login-validation.review-account-info.table.header.action": "Override",
"external-login-validation.review-account-info.table.row.not-applicable": "N/A",
"on-label": "ON", "on-label": "ON",
"off-label": "OFF", "off-label": "OFF",
@@ -6852,6 +6858,22 @@
"external-login-page.provide-email.create-account.notifications.error.content": "Please check again your email address and try again.", "external-login-page.provide-email.create-account.notifications.error.content": "Please check again your email address and try again.",
"external-login-page.confirm-email.create-account.notifications.error.no-netId": "Something went wrong with this email account. Try again or use a different method to login." "external-login-page.confirm-email.create-account.notifications.error.no-netId": "Something went wrong with this email account. Try again or use a different method to login.",
"external-login-page.orcid-confirmation.firstname": "First name",
"external-login-page.orcid-confirmation.firstname.label": "First name",
"external-login-page.orcid-confirmation.lastname": "Last name",
"external-login-page.orcid-confirmation.lastname.label": "Last name",
"external-login-page.orcid-confirmation.netid": "Account Identifier",
"external-login-page.orcid-confirmation.netid.placeholder": "xxxx-xxxx-xxxx-xxxx",
"external-login-page.orcid-confirmation.email": "Email",
"external-login-page.orcid-confirmation.email.label": "Email",
} }

View File

@@ -7864,10 +7864,18 @@
// TODO New key - Add a translation // TODO New key - Add a translation
"external-login.haveEmail.informationText": "It seems that you have not yet an account in this system. If this is the case, please confirm the data received from {{authMethod}} and a new account will be created for you. Otherwise, if you already have an account in the system, please update the email address to match the one already in use in the system or login via a different method to associate your {{authMethod}} to your existing account.", "external-login.haveEmail.informationText": "It seems that you have not yet an account in this system. If this is the case, please confirm the data received from {{authMethod}} and a new account will be created for you. Otherwise, if you already have an account in the system, please update the email address to match the one already in use in the system or login via a different method to associate your {{authMethod}} to your existing account.",
// "external-login.component.or": "or"
// TODO New key - Add a translation
"external-login.component.or": "or",
// "external-login.confirm-email.header": "Confirm or update email", // "external-login.confirm-email.header": "Confirm or update email",
// TODO New key - Add a translation // TODO New key - Add a translation
"external-login.confirm-email.header": "Confirm or update email", "external-login.confirm-email.header": "Confirm or update email",
// "external-login.confirmation.email-label": "User Email",
// TODO New key - Add a translation
"external-login.confirmation.email-label": "User Email",
// "external-login.confirmation.email-required": "Email is required.", // "external-login.confirmation.email-required": "Email is required.",
// TODO New key - Add a translation // TODO New key - Add a translation
"external-login.confirmation.email-required": "Email is required.", "external-login.confirmation.email-required": "Email is required.",
@@ -7920,6 +7928,10 @@
// TODO New key - Add a translation // TODO New key - Add a translation
"external-login-validation.review-account-info.table.header.action": "Override", "external-login-validation.review-account-info.table.header.action": "Override",
// "external-login-validation.review-account-info.table.row.not-applicable": "N/A",
// TODO New key - Add a translation
"external-login-validation.review-account-info.table.row.not-applicable": "N/A",
// "on-label": "ON", // "on-label": "ON",
// TODO New key - Add a translation // TODO New key - Add a translation
"on-label": "ON", "on-label": "ON",
@@ -7967,4 +7979,36 @@
// "external-login-page.confirm-email.create-account.notifications.error.no-netId": "Something went wrong with this email account. Try again or use a different method to login.", // "external-login-page.confirm-email.create-account.notifications.error.no-netId": "Something went wrong with this email account. Try again or use a different method to login.",
// TODO New key - Add a translation // TODO New key - Add a translation
"external-login-page.confirm-email.create-account.notifications.error.no-netId": "Something went wrong with this email account. Try again or use a different method to login.", "external-login-page.confirm-email.create-account.notifications.error.no-netId": "Something went wrong with this email account. Try again or use a different method to login.",
//"external-login-page.orcid-confirmation.firstname": "First name",
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.firstname": "First name",
//"external-login-page.orcid-confirmation.firstname.label": "First name",
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.firstname.label": "First name",
// "external-login-page.orcid-confirmation.lastname": "Last name",
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.lastname": "Last name",
// "external-login-page.orcid-confirmation.lastname.label": "Last name",
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.lastname.label": "Last name",
// "external-login-page.orcid-confirmation.netid": "Account Identifier",
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.netid": "Account Identifier",
// "external-login-page.orcid-confirmation.netid.placeholder": "xxxx-xxxx-xxxx-xxxx",
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.netid.placeholder": "xxxx-xxxx-xxxx-xxxx",
// "external-login-page.orcid-confirmation.email": "Email",
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.email": "Email",
// "external-login-page.orcid-confirmation.email.label": "Email"
// TODO New key - Add a translation
"external-login-page.orcid-confirmation.email.label": "Email"
} }