diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 08101260dc..f1c4ebd121 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -163,6 +163,7 @@ import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-li import { ConfigurationDataService } from './data/configuration-data.service'; import { ConfigurationProperty } from './shared/configuration-property.model'; import { UserAgreementGuard } from './user-agreement/user-agreement.guard'; +import { UserAgreementService } from './user-agreement/user-agreement.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -291,6 +292,7 @@ const PROVIDERS = [ MetadataFieldDataService, TokenResponseParsingService, UserAgreementGuard, + UserAgreementService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/user-agreement/user-agreement.guard.ts b/src/app/core/user-agreement/user-agreement.guard.ts index d955c300a1..7464b87e00 100644 --- a/src/app/core/user-agreement/user-agreement.guard.ts +++ b/src/app/core/user-agreement/user-agreement.guard.ts @@ -1,14 +1,9 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; import { Observable } from 'rxjs/internal/Observable'; -import { CookieService } from '../services/cookie.service'; -import { AuthService } from '../auth/auth.service'; -import { map } from 'rxjs/operators'; -import { hasValue } from '../../shared/empty.util'; import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/operators'; - -export const USER_AGREEMENT_COOKIE = 'hasAgreedEndUser'; -export const USER_AGREEMENT_METADATA_FIELD = 'dspace.agreements.end-user'; +import { UserAgreementService } from './user-agreement.service'; +import { tap } from 'rxjs/operators'; /** * A guard redirecting users to the end agreement page when they haven't accepted the latest user agreement @@ -16,22 +11,24 @@ export const USER_AGREEMENT_METADATA_FIELD = 'dspace.agreements.end-user'; @Injectable() export class UserAgreementGuard implements CanActivate { - constructor(protected cookie: CookieService, - protected authService: AuthService, + constructor(protected userAgreementService: UserAgreementService, protected router: Router) { } /** * True when the user has accepted the agreements + * The user will be redirected to the End User Agreement page if they haven't accepted it before + * A redirect URL will be provided with the navigation so the component can redirect the user back to the blocked route + * when they're finished accepting the agreement */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean { - if (this.cookie.get(USER_AGREEMENT_COOKIE) === true) { - return true; - } else { - return this.authService.getAuthenticatedUserFromStore().pipe( - map((user) => hasValue(user) && user.hasMetadata(USER_AGREEMENT_METADATA_FIELD) && user.firstMetadata(USER_AGREEMENT_METADATA_FIELD).value === 'true'), - returnEndUserAgreementUrlTreeOnFalse(this.router) - ); - } + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.userAgreementService.hasCurrentUserAcceptedAgreement().pipe( + returnEndUserAgreementUrlTreeOnFalse(this.router), + tap((result) => { + if (result instanceof UrlTree) { + this.router.navigateByUrl(result, { state: { redirect: state.url } }) + } + }) + ); } } diff --git a/src/app/core/user-agreement/user-agreement.service.ts b/src/app/core/user-agreement/user-agreement.service.ts new file mode 100644 index 0000000000..8e5694d81c --- /dev/null +++ b/src/app/core/user-agreement/user-agreement.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { AuthService } from '../auth/auth.service'; +import { CookieService } from '../services/cookie.service'; +import { Observable } from 'rxjs/internal/Observable'; +import { of as observableOf } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { cloneDeep } from 'lodash'; +import { Metadata } from '../shared/metadata.utils'; +import { EPersonDataService } from '../eperson/eperson-data.service'; +import { getSucceededRemoteData } from '../shared/operators'; + +export const USER_AGREEMENT_COOKIE = 'hasAgreedEndUser'; +export const USER_AGREEMENT_METADATA_FIELD = 'dspace.agreements.end-user'; + +/** + * Service for checking and managing the status of the current end user agreement + */ +@Injectable() +export class UserAgreementService { + + constructor(protected cookie: CookieService, + protected authService: AuthService, + protected ePersonService: EPersonDataService) { + } + + /** + * Whether or not the current user has accepted the End User Agreement + */ + hasCurrentUserAcceptedAgreement(): Observable { + if (this.cookie.get(USER_AGREEMENT_COOKIE) === true) { + return observableOf(true); + } else { + return this.authService.isAuthenticated().pipe( + switchMap((authenticated) => { + if (authenticated) { + return this.authService.getAuthenticatedUserFromStore().pipe( + map((user) => hasValue(user) && user.hasMetadata(USER_AGREEMENT_METADATA_FIELD) && user.firstMetadata(USER_AGREEMENT_METADATA_FIELD).value === 'true') + ); + } else { + return observableOf(false); + } + }) + ); + } + } + + /** + * Set the current user's accepted agreement status + * When a user is authenticated, set his/her metadata to the provided value + * When no user is authenticated, set the cookie to the provided value + * @param accepted + */ + setUserAcceptedAgreement(accepted: boolean): Observable { + return this.authService.isAuthenticated().pipe( + switchMap((authenticated) => { + if (authenticated) { + return this.authService.getAuthenticatedUserFromStore().pipe( + switchMap((user) => { + const updatedUser = cloneDeep(user); + Metadata.setFirstValue(updatedUser.metadata, USER_AGREEMENT_METADATA_FIELD, String(accepted)); + return this.ePersonService.update(updatedUser); + }), + getSucceededRemoteData(), + map((rd) => hasValue(rd.payload)) + ); + } else { + this.cookie.set(USER_AGREEMENT_COOKIE, accepted); + return observableOf(true); + } + }), + take(1) + ); + } + +} diff --git a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.ts b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.ts index cbfc706229..faa7d5a78f 100644 --- a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.ts +++ b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.ts @@ -5,5 +5,8 @@ import { Component } from '@angular/core'; templateUrl: './end-user-agreement-content.component.html', styleUrls: ['./end-user-agreement-content.component.scss'] }) +/** + * Component displaying the contents of the End User Agreement + */ export class EndUserAgreementContentComponent { } diff --git a/src/app/info/end-user-agreement/end-user-agreement.component.html b/src/app/info/end-user-agreement/end-user-agreement.component.html index cc155ee9c3..624264e2cc 100644 --- a/src/app/info/end-user-agreement/end-user-agreement.component.html +++ b/src/app/info/end-user-agreement/end-user-agreement.component.html @@ -1,3 +1,13 @@
+ +
+ + + +
+ + +
+
diff --git a/src/app/info/end-user-agreement/end-user-agreement.component.scss b/src/app/info/end-user-agreement/end-user-agreement.component.scss index e69de29bb2..2960a0fac1 100644 --- a/src/app/info/end-user-agreement/end-user-agreement.component.scss +++ b/src/app/info/end-user-agreement/end-user-agreement.component.scss @@ -0,0 +1,8 @@ +input#user-agreement-accept { + /* Large-sized Checkboxes */ + -ms-transform: scale(1.6); /* IE */ + -moz-transform: scale(1.6); /* FF */ + -webkit-transform: scale(1.6); /* Safari and Chrome */ + -o-transform: scale(1.6); /* Opera */ + padding: 10px; +} diff --git a/src/app/info/end-user-agreement/end-user-agreement.component.ts b/src/app/info/end-user-agreement/end-user-agreement.component.ts index 3e43d68784..5da59bf5c4 100644 --- a/src/app/info/end-user-agreement/end-user-agreement.component.ts +++ b/src/app/info/end-user-agreement/end-user-agreement.component.ts @@ -1,9 +1,85 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { AuthService } from '../../core/auth/auth.service'; +import { take } from 'rxjs/operators'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../app.reducer'; +import { LogOutAction } from '../../core/auth/auth.actions'; +import { UserAgreementService } from '../../core/user-agreement/user-agreement.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { hasValue } from '../../shared/empty.util'; @Component({ selector: 'ds-end-user-agreement', templateUrl: './end-user-agreement.component.html', styleUrls: ['./end-user-agreement.component.scss'] }) -export class EndUserAgreementComponent { +/** + * Component displaying the End User Agreement and an option to accept it + */ +export class EndUserAgreementComponent implements OnInit { + + /** + * Whether or not the user agreement has been accepted + */ + accepted = false; + + constructor(protected userAgreementService: UserAgreementService, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected authService: AuthService, + protected store: Store, + protected router: Router) { + } + + /** + * Initialize the component + */ + ngOnInit(): void { + this.initAccepted(); + } + + /** + * Initialize the "accepted" property of this component by checking if the current user has accepted it before + */ + initAccepted() { + this.userAgreementService.hasCurrentUserAcceptedAgreement().subscribe((accepted) => { + this.accepted = accepted; + }); + } + + /** + * Submit the form + * Set the End User Agreement, display a notification and (optionally) redirect the user back to their original destination + */ + submit() { + this.userAgreementService.setUserAcceptedAgreement(this.accepted).subscribe((success) => { + if (success) { + this.notificationsService.success(this.translate.instant('info.end-user-agreement.accept.success')); + const redirect = window.history.state.redirect; + if (hasValue(redirect)) { + this.router.navigateByUrl(redirect); + } + } else { + this.notificationsService.error(this.translate.instant('info.end-user-agreement.accept.error')); + } + }); + } + + /** + * Cancel the agreement + * If the user is logged in, this will log him/her out + * If the user is not logged in, they will be redirected to the homepage + */ + cancel() { + this.authService.isAuthenticated().pipe(take(1)).subscribe((authenticated) => { + if (authenticated) { + this.store.dispatch(new LogOutAction()); + } else { + this.router.navigate(['home']); + } + }); + } + } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 510163c6c3..ca82aca80b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1138,8 +1138,18 @@ + "info.end-user-agreement.accept": "I have read and I agree to the End User Agreement", + + "info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement", + + "info.end-user-agreement.accept.success": "Successfully updated the End User Agreement", + "info.end-user-agreement.breadcrumbs": "End User Agreement", + "info.end-user-agreement.buttons.cancel": "Cancel", + + "info.end-user-agreement.buttons.save": "Save", + "info.end-user-agreement.head": "End User Agreement", "info.end-user-agreement.title": "End User Agreement",