72541: Accepting the End User Agreement + UserAgreementService

This commit is contained in:
Kristof De Langhe
2020-08-20 17:28:25 +02:00
parent 8768645e4a
commit d46355e274
8 changed files with 202 additions and 20 deletions

View File

@@ -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,

View File

@@ -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 | UrlTree> | 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<boolean | UrlTree> {
return this.userAgreementService.hasCurrentUserAcceptedAgreement().pipe(
returnEndUserAgreementUrlTreeOnFalse(this.router),
tap((result) => {
if (result instanceof UrlTree) {
this.router.navigateByUrl(result, { state: { redirect: state.url } })
}
})
);
}
}

View File

@@ -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<boolean> {
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<boolean> {
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)
);
}
}

View File

@@ -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 {
}

View File

@@ -1,3 +1,13 @@
<div class="container">
<ds-end-user-agreement-content></ds-end-user-agreement-content>
<form class="form-user-agreement-accept mt-4" (ngSubmit)="submit()" novalidate>
<input class="ml-1 mr-2" type="checkbox" id="user-agreement-accept" [(ngModel)]="accepted" [ngModelOptions]="{standalone: true}">
<label class="col-form-label-lg" for="user-agreement-accept">{{ 'info.end-user-agreement.accept' | translate }}</label>
<div class="d-flex">
<button type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button>
<button type="submit" class="btn btn-primary" [disabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button>
</div>
</form>
</div>

View File

@@ -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;
}

View File

@@ -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<AppState>,
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']);
}
});
}
}

View File

@@ -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",