Skip to content

Commit 214a77a

Browse files
alisaismailativins01-4science
authored andcommitted
[CST-14902][CST-15073][CST-15074] Adds ORCID login flow with private email
1 parent fca5700 commit 214a77a

89 files changed

Lines changed: 3454 additions & 278 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/app-routes.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
InMemoryScrollingOptions,
3-
Route,
4-
RouterConfigOptions,
5-
} from '@angular/router';
1+
import { InMemoryScrollingOptions, Route, RouterConfigOptions, } from '@angular/router';
62

73
import { NOTIFICATIONS_MODULE_PATH } from './admin/admin-routing-paths';
84
import {
@@ -25,8 +21,12 @@ import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routin
2521
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
2622
import { authBlockingGuard } from './core/auth/auth-blocking.guard';
2723
import { authenticatedGuard } from './core/auth/authenticated.guard';
28-
import { groupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
29-
import { siteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
24+
import {
25+
groupAdministratorGuard
26+
} from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
27+
import {
28+
siteAdministratorGuard
29+
} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
3030
import { siteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
3131
import { endUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
3232
import { reloadGuard } from './core/reload/reload.guard';
@@ -37,7 +37,9 @@ import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
3737
import { menuResolver } from './menuResolver';
3838
import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
3939
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
40-
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
40+
import {
41+
ThemedPageInternalServerErrorComponent
42+
} from './page-internal-server-error/themed-page-internal-server-error.component';
4143
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
4244
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
4345
import { provideSubmissionState } from './submission/provide-submission-state';
@@ -257,6 +259,20 @@ export const APP_ROUTES: Route[] = [
257259
.then((m) => m.ROUTES),
258260
canActivate: [authenticatedGuard],
259261
},
262+
{
263+
path: 'external-login/:token',
264+
loadChildren: () => import('./external-login-page/external-login-routes').then((m) => m.ROUTES)
265+
},
266+
{
267+
path: 'review-account/:token',
268+
loadChildren: () => import('./external-login-review-account-info-page/external-login-review-account-info-page-routes')
269+
.then((m) => m.ROUTES)
270+
},
271+
{
272+
path: 'email-confirmation',
273+
loadChildren: () => import('./external-login-email-confirmation-page/external-login-email-confirmation-page-routes')
274+
.then((m) => m.ROUTES)
275+
},
260276
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
261277
],
262278
},

src/app/core/auth/auth-request.service.ts

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,30 @@
11
import { Observable } from 'rxjs';
2-
import {
3-
distinctUntilChanged,
4-
filter,
5-
map,
6-
switchMap,
7-
take,
8-
tap,
9-
} from 'rxjs/operators';
10-
11-
import { isNotEmpty } from '../../shared/empty.util';
2+
import { distinctUntilChanged, filter, map, switchMap, take, tap, } from 'rxjs/operators';
3+
4+
import { isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
125
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
13-
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
14-
import { RemoteData } from '../data/remote-data';
15-
import {
16-
GetRequest,
17-
PostRequest,
18-
} from '../data/request.models';
6+
import { HALEndpointService } from '../shared/hal-endpoint.service';
197
import { RequestService } from '../data/request.service';
20-
import { RestRequest } from '../data/rest-request.model';
8+
import { DeleteRequest, GetRequest, PostRequest } from '../data/request.models';
219
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
22-
import { HALEndpointService } from '../shared/hal-endpoint.service';
2310
import { getFirstCompletedRemoteData } from '../shared/operators';
11+
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
12+
import { RemoteData } from '../data/remote-data';
13+
import { RestRequest } from '../data/rest-request.model';
2414
import { URLCombiner } from '../url-combiner/url-combiner';
2515
import { AuthStatus } from './models/auth-status.model';
2616
import { ShortLivedToken } from './models/short-lived-token.model';
17+
import { MachineToken } from './models/machine-token.model';
18+
import { NoContent } from '../shared/NoContent.model';
19+
import { sendRequest } from '../shared/request.operators';
2720

2821
/**
2922
* Abstract service to send authentication requests
3023
*/
3124
export abstract class AuthRequestService {
3225
protected linkName = 'authn';
3326
protected shortlivedtokensEndpoint = 'shortlivedtokens';
27+
protected machinetokenEndpoint = 'machinetokens';
3428

3529
constructor(protected halService: HALEndpointService,
3630
protected requestService: RequestService,
@@ -139,4 +133,32 @@ export abstract class AuthRequestService {
139133
}),
140134
);
141135
}
136+
137+
/**
138+
* Send a post request to create a machine token
139+
*/
140+
public postToMachineTokenEndpoint(): Observable<RemoteData<MachineToken>> {
141+
return this.halService.getEndpoint(this.linkName).pipe(
142+
isNotEmptyOperator(),
143+
distinctUntilChanged(),
144+
map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()),
145+
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
146+
tap((request: RestRequest) => this.requestService.send(request)),
147+
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<MachineToken>(request.uuid))
148+
);
149+
}
150+
151+
/**
152+
* Send a delete request to destroy a machine token
153+
*/
154+
public deleteToMachineTokenEndpoint(): Observable<RemoteData<NoContent>> {
155+
return this.halService.getEndpoint(this.linkName).pipe(
156+
isNotEmptyOperator(),
157+
distinctUntilChanged(),
158+
map((href: string) => new URLCombiner(href, this.machinetokenEndpoint).toString()),
159+
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
160+
sendRequest(this.requestService),
161+
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<MachineToken>(request.uuid)),
162+
);
163+
}
142164
}

src/app/core/auth/auth.service.ts

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,14 @@
11
import { HttpHeaders } from '@angular/common/http';
2-
import {
3-
Inject,
4-
Injectable,
5-
Optional,
6-
} from '@angular/core';
2+
import { Inject, Injectable, Optional, } from '@angular/core';
73
import { Router } from '@angular/router';
8-
import {
9-
select,
10-
Store,
11-
} from '@ngrx/store';
4+
import { select, Store, } from '@ngrx/store';
125
import { TranslateService } from '@ngx-translate/core';
136
import { CookieAttributes } from 'js-cookie';
14-
import {
15-
Observable,
16-
of as observableOf,
17-
} from 'rxjs';
18-
import {
19-
filter,
20-
map,
21-
startWith,
22-
switchMap,
23-
take,
24-
} from 'rxjs/operators';
7+
import { Observable, of as observableOf, } from 'rxjs';
8+
import { filter, map, startWith, switchMap, take, } from 'rxjs/operators';
259

2610
import { environment } from '../../../environments/environment';
27-
import {
28-
REQUEST,
29-
RESPONSE,
30-
} from '../../../express.tokens';
11+
import { REQUEST, RESPONSE, } from '../../../express.tokens';
3112
import { AppState } from '../../app.reducer';
3213
import {
3314
hasNoValue,
@@ -41,10 +22,7 @@ import {
4122
import { NotificationsService } from '../../shared/notifications/notifications.service';
4223
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
4324
import { followLink } from '../../shared/utils/follow-link-config.model';
44-
import {
45-
buildPaginatedList,
46-
PaginatedList,
47-
} from '../data/paginated-list.model';
25+
import { buildPaginatedList, PaginatedList, } from '../data/paginated-list.model';
4826
import { RemoteData } from '../data/remote-data';
4927
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
5028
import { EPersonDataService } from '../eperson/eperson-data.service';
@@ -54,13 +32,16 @@ import { CookieService } from '../services/cookie.service';
5432
import { HardRedirectService } from '../services/hard-redirect.service';
5533
import { RouteService } from '../services/route.service';
5634
import {
57-
NativeWindowRef,
58-
NativeWindowService,
59-
} from '../services/window.service';
60-
import {
61-
getAllSucceededRemoteDataPayload,
62-
getFirstCompletedRemoteData,
63-
} from '../shared/operators';
35+
getAuthenticatedUserId,
36+
getAuthenticationToken,
37+
getExternalAuthCookieStatus,
38+
getRedirectUrl,
39+
isAuthenticated,
40+
isAuthenticatedLoaded,
41+
isIdle,
42+
isTokenRefreshing
43+
} from './selectors';
44+
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../shared/operators';
6445
import { PageInfo } from '../shared/page-info.model';
6546
import {
6647
CheckAuthenticationTokenAction,
@@ -74,20 +55,11 @@ import {
7455
import { AuthRequestService } from './auth-request.service';
7556
import { AuthMethod } from './models/auth.method';
7657
import { AuthStatus } from './models/auth-status.model';
77-
import {
78-
AuthTokenInfo,
79-
TOKENITEM,
80-
} from './models/auth-token-info.model';
81-
import {
82-
getAuthenticatedUserId,
83-
getAuthenticationToken,
84-
getExternalAuthCookieStatus,
85-
getRedirectUrl,
86-
isAuthenticated,
87-
isAuthenticatedLoaded,
88-
isIdle,
89-
isTokenRefreshing,
90-
} from './selectors';
58+
import { AuthTokenInfo, TOKENITEM, } from './models/auth-token-info.model';
59+
import { NoContent } from '../shared/NoContent.model';
60+
import { URLCombiner } from '../url-combiner/url-combiner';
61+
import { MachineToken } from './models/machine-token.model';
62+
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
9163

9264
export const LOGIN_ROUTE = '/login';
9365
export const LOGOUT_ROUTE = '/logout';
@@ -579,6 +551,31 @@ export class AuthService {
579551
});
580552
}
581553

554+
/**
555+
* Returns the external server redirect URL.
556+
* @param origin - The origin route.
557+
* @param redirectRoute - The redirect route.
558+
* @param location - The location.
559+
* @returns The external server redirect URL.
560+
*/
561+
getExternalServerRedirectUrl(origin: string, redirectRoute: string, location: string): string {
562+
const correctRedirectUrl = new URLCombiner(origin, redirectRoute).toString();
563+
564+
let externalServerUrl = location;
565+
const myRegexp = /\?redirectUrl=(.*)/g;
566+
const match = myRegexp.exec(location);
567+
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
568+
569+
// Check whether the current page is different from the redirect url received from rest
570+
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
571+
// change the redirect url with the current page url
572+
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
573+
externalServerUrl = location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
574+
}
575+
576+
return externalServerUrl;
577+
}
578+
582579
/**
583580
* Clear redirect url
584581
*/
@@ -664,4 +661,18 @@ export class AuthService {
664661
}
665662
}
666663

664+
/**
665+
* Create a new machine token for the current user
666+
*/
667+
public createMachineToken(): Observable<RemoteData<MachineToken>> {
668+
return this.authRequestService.postToMachineTokenEndpoint();
669+
}
670+
671+
/**
672+
* Delete the machine token for the current user
673+
*/
674+
public deleteMachineToken(): Observable<RemoteData<NoContent>> {
675+
return this.authRequestService.deleteToMachineTokenEndpoint();
676+
}
677+
667678
}

src/app/core/auth/models/auth.method-type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ export enum AuthMethodType {
55
Ip = 'ip',
66
X509 = 'x509',
77
Oidc = 'oidc',
8-
Orcid = 'orcid'
8+
Orcid = 'orcid',
99
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum AuthRegistrationType {
2+
Orcid = 'ORCID',
3+
Validation = 'VALIDATION_',
4+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { autoserialize, autoserializeAs, deserialize } from 'cerialize';
2+
3+
import { typedObject } from '../../cache/builders/build-decorators';
4+
import { CacheableObject } from '../../cache/cacheable-object.model';
5+
import { excludeFromEquals } from '../../utilities/equals.decorators';
6+
import { ResourceType } from '../../shared/resource-type';
7+
import { HALLink } from '../../shared/hal-link.model';
8+
import { MACHINE_TOKEN } from './machine-token.resource-type';
9+
10+
/**
11+
* A machine token that can be used to authenticate a rest request
12+
*/
13+
@typedObject
14+
export class MachineToken implements CacheableObject {
15+
static type = MACHINE_TOKEN;
16+
/**
17+
* The type for this MachineToken
18+
*/
19+
@excludeFromEquals
20+
@autoserialize
21+
type: ResourceType;
22+
23+
/**
24+
* The value for this MachineToken
25+
*/
26+
@autoserializeAs('token')
27+
value: string;
28+
29+
/**
30+
* The {@link HALLink}s for this MachineToken
31+
*/
32+
@deserialize
33+
_links: {
34+
self: HALLink;
35+
};
36+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ResourceType } from '../../shared/resource-type';
2+
3+
/**
4+
* The resource type for MachineToken
5+
*
6+
* Needs to be in a separate file to prevent circular
7+
* dependencies in webpack.
8+
*/
9+
export const MACHINE_TOKEN = new ResourceType('machinetoken');

src/app/core/data/eperson-registration.service.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('EpersonRegistrationService', () => {
105105

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

110110
expect(expected).toBeObservable(cold('(a|)', {
111111
a: jasmine.objectContaining({
@@ -123,7 +123,7 @@ describe('EpersonRegistrationService', () => {
123123
testScheduler.run(({ cold, expectObservable }) => {
124124
rdbService.buildSingle.and.returnValue(cold('a', { a: rd }));
125125

126-
service.searchByToken('test-token');
126+
service.searchByTokenAndUpdateData('test-token');
127127

128128
expect(requestService.send).toHaveBeenCalledWith(
129129
jasmine.objectContaining({

0 commit comments

Comments
 (0)