mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 18:44:14 +00:00
Merge pull request #861 from atmire/Cookie-preferences
Cookie consent preferences
This commit is contained in:
@@ -97,6 +97,7 @@
|
|||||||
"json5": "^2.1.0",
|
"json5": "^2.1.0",
|
||||||
"jsonschema": "1.2.2",
|
"jsonschema": "1.2.2",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
|
"klaro": "^0.6.3",
|
||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"morgan": "^1.9.1",
|
"morgan": "^1.9.1",
|
||||||
"ng-mocks": "^8.1.0",
|
"ng-mocks": "^8.1.0",
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { delay, map, distinctUntilChanged } from 'rxjs/operators';
|
import { delay, map, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
HostListener,
|
HostListener,
|
||||||
Inject,
|
Inject,
|
||||||
OnInit,
|
OnInit, Optional,
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
||||||
@@ -31,8 +31,8 @@ import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
|||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { models } from './core/core.module';
|
import { models } from './core/core.module';
|
||||||
import { LocaleService } from './core/locale/locale.service';
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
|
import { hasValue } from './shared/empty.util';
|
||||||
export const LANG_COOKIE = 'language_cookie';
|
import { KlaroService } from './shared/cookies/klaro.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -69,8 +69,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private cssService: CSSVariableService,
|
private cssService: CSSVariableService,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private localeService: LocaleService
|
private localeService: LocaleService,
|
||||||
|
@Optional() private cookiesService: KlaroService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/* Use models object so all decorators are actually called */
|
/* Use models object so all decorators are actually called */
|
||||||
this.models = models;
|
this.models = models;
|
||||||
// Load all the languages that are defined as active from the config file
|
// Load all the languages that are defined as active from the config file
|
||||||
@@ -91,6 +93,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
console.info(environment);
|
console.info(environment);
|
||||||
}
|
}
|
||||||
this.storeCSSVariables();
|
this.storeCSSVariables();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -98,6 +101,12 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
map((isBlocking: boolean) => isBlocking === false),
|
map((isBlocking: boolean) => isBlocking === false),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
this.isNotAuthBlocking$
|
||||||
|
.pipe(
|
||||||
|
filter((notBlocking: boolean) => notBlocking),
|
||||||
|
take(1)
|
||||||
|
).subscribe(() => this.initializeKlaro());
|
||||||
|
|
||||||
const env: string = environment.production ? 'Production' : 'Development';
|
const env: string = environment.production ? 'Production' : 'Development';
|
||||||
const color: string = environment.production ? 'red' : 'green';
|
const color: string = environment.production ? 'red' : 'green';
|
||||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||||
@@ -158,4 +167,9 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializeKlaro() {
|
||||||
|
if (hasValue(this.cookiesService)) {
|
||||||
|
this.cookiesService.initialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -382,6 +382,14 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createPatchFromCache(object: T): Observable<Operation[]> {
|
||||||
|
const oldVersion$ = this.findByHref(object._links.self.href);
|
||||||
|
return oldVersion$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((oldVersion: T) => this.comparator.diff(oldVersion, object)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a PUT request for the specified object
|
* Send a PUT request for the specified object
|
||||||
*
|
*
|
||||||
@@ -410,18 +418,16 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* @param {DSpaceObject} object The given object
|
* @param {DSpaceObject} object The given object
|
||||||
*/
|
*/
|
||||||
update(object: T): Observable<RemoteData<T>> {
|
update(object: T): Observable<RemoteData<T>> {
|
||||||
const oldVersion$ = this.findByHref(object._links.self.href);
|
return this.createPatchFromCache(object)
|
||||||
return oldVersion$.pipe(
|
.pipe(
|
||||||
getSucceededRemoteData(),
|
mergeMap((operations: Operation[]) => {
|
||||||
getRemoteDataPayload(),
|
|
||||||
mergeMap((oldVersion: T) => {
|
|
||||||
const operations = this.comparator.diff(oldVersion, object);
|
|
||||||
if (isNotEmpty(operations)) {
|
if (isNotEmpty(operations)) {
|
||||||
this.objectCache.addPatch(object._links.self.href, operations);
|
this.objectCache.addPatch(object._links.self.href, operations);
|
||||||
}
|
}
|
||||||
return this.findByHref(object._links.self.href);
|
return this.findByHref(object._links.self.href);
|
||||||
}
|
}
|
||||||
));
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -10,7 +10,7 @@ import { Observable, of as observableOf, combineLatest } from 'rxjs';
|
|||||||
import { map, take, flatMap } from 'rxjs/operators';
|
import { map, take, flatMap } from 'rxjs/operators';
|
||||||
import { NativeWindowService, NativeWindowRef } from '../services/window.service';
|
import { NativeWindowService, NativeWindowRef } from '../services/window.service';
|
||||||
|
|
||||||
export const LANG_COOKIE = 'language_cookie';
|
export const LANG_COOKIE = 'dsLanguage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This enum defines the possible origin of the languages
|
* This enum defines the possible origin of the languages
|
||||||
|
@@ -184,4 +184,25 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
|
|||||||
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||||
return [this.constructor as GenericConstructor<ListableObject>];
|
return [this.constructor as GenericConstructor<ListableObject>];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMetadata(key: string, language?: string, ...values: string[]) {
|
||||||
|
const mdValues: MetadataValue[] = values.map((value: string, index: number) => {
|
||||||
|
const md = new MetadataValue();
|
||||||
|
md.value = value;
|
||||||
|
md.authority = null;
|
||||||
|
md.confidence = -1;
|
||||||
|
md.language = language || null;
|
||||||
|
md.place = index;
|
||||||
|
return md;
|
||||||
|
});
|
||||||
|
if (hasNoValue(this.metadata)) {
|
||||||
|
this.metadata = Object.create({});
|
||||||
|
}
|
||||||
|
this.metadata[key] = mdValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMetadata(key: string) {
|
||||||
|
delete this.metadata[key];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,16 @@
|
|||||||
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
|
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
|
||||||
<a href="http://www.duraspace.org/">{{ 'footer.link.duraspace' | translate}}</a>
|
<a href="http://www.duraspace.org/">{{ 'footer.link.duraspace' | translate}}</a>
|
||||||
</p>
|
</p>
|
||||||
|
<ul class="list-unstyled small d-flex justify-content-center mb-0 text-secondary">
|
||||||
|
<li>
|
||||||
|
<a href="#" (click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@@ -8,11 +8,33 @@ $footer-logo-height: 55px;
|
|||||||
border-top: $footer-border;
|
border-top: $footer-border;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $footer-padding;
|
padding: $footer-padding;
|
||||||
|
padding-bottom: $spacer;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: $footer-logo-height;
|
height: $footer-logo-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-top: $spacer * 0.5;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-flex;
|
||||||
|
a {
|
||||||
|
padding: 0 $spacer/2;
|
||||||
|
color: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
border-right: 1px map-get($theme-colors, secondary) solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Optional } from '@angular/core';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-footer',
|
selector: 'ds-footer',
|
||||||
@@ -6,7 +8,15 @@ import { Component } from '@angular/core';
|
|||||||
templateUrl: 'footer.component.html'
|
templateUrl: 'footer.component.html'
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent {
|
||||||
|
|
||||||
dateObj: number = Date.now();
|
dateObj: number = Date.now();
|
||||||
|
|
||||||
|
constructor(@Optional() private cookies: KlaroService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
showCookieSettings() {
|
||||||
|
if (hasValue(this.cookies)) {
|
||||||
|
this.cookies.showSettings();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
237
src/app/shared/cookies/browser-klaro.service.spec.ts
Normal file
237
src/app/shared/cookies/browser-klaro.service.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BrowserKlaroService, COOKIE_MDFIELD } from './browser-klaro.service';
|
||||||
|
import { getMockTranslateService } from '../mocks/translate.service.mock';
|
||||||
|
import { of as observableOf } from 'rxjs'
|
||||||
|
import { RestResponse } from '../../core/cache/response.models';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { CookieService } from '../../core/services/cookie.service';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
|
describe('BrowserKlaroService', () => {
|
||||||
|
let translateService;
|
||||||
|
let ePersonService;
|
||||||
|
let authService;
|
||||||
|
let cookieService;
|
||||||
|
|
||||||
|
let user;
|
||||||
|
let service: BrowserKlaroService;
|
||||||
|
|
||||||
|
let mockConfig;
|
||||||
|
let appName;
|
||||||
|
let purpose;
|
||||||
|
let testKey;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = new EPerson();
|
||||||
|
|
||||||
|
translateService = getMockTranslateService();
|
||||||
|
ePersonService = jasmine.createSpyObj('ePersonService', {
|
||||||
|
createPatchFromCache: observableOf([]),
|
||||||
|
patch: observableOf(new RestResponse(true, 200, 'Ok'))
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
getAuthenticatedUserFromStore: observableOf(user)
|
||||||
|
});
|
||||||
|
cookieService = jasmine.createSpyObj('cookieService', {
|
||||||
|
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
||||||
|
set: () => {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
BrowserKlaroService,
|
||||||
|
{
|
||||||
|
provide: TranslateService,
|
||||||
|
useValue: translateService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: EPersonDataService,
|
||||||
|
useValue: ePersonService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: authService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CookieService,
|
||||||
|
useValue: cookieService
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
service = TestBed.get(BrowserKlaroService);
|
||||||
|
appName = 'testName';
|
||||||
|
purpose = 'test purpose';
|
||||||
|
testKey = 'this.is.a.fake.message.key';
|
||||||
|
|
||||||
|
mockConfig = {
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
purposes: {},
|
||||||
|
test: {
|
||||||
|
testeritis: testKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apps: [{
|
||||||
|
name: appName,
|
||||||
|
purposes: [purpose]
|
||||||
|
}],
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
service.klaroConfig = mockConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialize with user', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
||||||
|
translateService.get.and.returnValue(observableOf('loading...'));
|
||||||
|
spyOn(service, 'addAppMessages');
|
||||||
|
spyOn((service as any), 'initializeUser');
|
||||||
|
spyOn(service, 'translateConfiguration');
|
||||||
|
});
|
||||||
|
it('to call the initialize user method and other methods', () => {
|
||||||
|
service.initialize();
|
||||||
|
expect((service as any).initializeUser).toHaveBeenCalledWith(user);
|
||||||
|
expect(service.addAppMessages).toHaveBeenCalled();
|
||||||
|
expect(service.translateConfiguration).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('to not call the initialize user method, but the other methods', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((service as any), 'getUser$').and.returnValue(observableOf(undefined));
|
||||||
|
translateService.get.and.returnValue(observableOf('loading...'));
|
||||||
|
spyOn(service, 'addAppMessages');
|
||||||
|
spyOn((service as any), 'initializeUser');
|
||||||
|
spyOn(service, 'translateConfiguration');
|
||||||
|
});
|
||||||
|
it('to call all ', () => {
|
||||||
|
service.initialize();
|
||||||
|
expect((service as any).initializeUser).not.toHaveBeenCalledWith(user);
|
||||||
|
expect(service.addAppMessages).toHaveBeenCalled();
|
||||||
|
expect(service.translateConfiguration).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('addAppMessages', () => {
|
||||||
|
service.addAppMessages();
|
||||||
|
expect(mockConfig.translations.en[appName]).toBeDefined();
|
||||||
|
expect(mockConfig.translations.en.purposes[purpose]).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('translateConfiguration', () => {
|
||||||
|
service.translateConfiguration();
|
||||||
|
expect((service as any).translateService.instant).toHaveBeenCalledWith(testKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeUser when there is a metadata field value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
user.setMetadata(COOKIE_MDFIELD, undefined, '{}');
|
||||||
|
spyOn(service, 'restoreSettingsForUsers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializeUser', () => {
|
||||||
|
(service as any).initializeUser(user);
|
||||||
|
expect(service.restoreSettingsForUsers).toHaveBeenCalledWith(user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeUser when there is no metadata field value but there is an anonymous cookie', () => {
|
||||||
|
const cookie = '{test: \'testt\'}';
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).cookieService.get.and.returnValue(cookie);
|
||||||
|
spyOn(service, 'updateSettingsForUsers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializeUser', () => {
|
||||||
|
(service as any).initializeUser(user);
|
||||||
|
expect((service as any).cookieService.set).toHaveBeenCalledWith(service.getStorageName(user.uuid), cookie)
|
||||||
|
expect(service.updateSettingsForUsers).toHaveBeenCalledWith(user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUser$ when there is no one authenticated', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).authService.isAuthenticated.and.returnValue(observableOf(false));
|
||||||
|
});
|
||||||
|
it('should return undefined', () => {
|
||||||
|
getTestScheduler().expectObservable((service as any).getUser$()).toBe('(a|)', { a: undefined });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUser$ when there someone is authenticated', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).authService.isAuthenticated.and.returnValue(observableOf(true));
|
||||||
|
(service as any).authService.getAuthenticatedUserFromStore.and.returnValue(observableOf(user));
|
||||||
|
});
|
||||||
|
it('should return the user', () => {
|
||||||
|
getTestScheduler().expectObservable((service as any).getUser$()).toBe('(a|)', { a: user });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSettingsForUser', () => {
|
||||||
|
const cookieConsentString = '{test: \'testt\'}';
|
||||||
|
beforeEach(() => {
|
||||||
|
user.metadata = {};
|
||||||
|
user.metadata[COOKIE_MDFIELD] = [Object.assign(new MetadataValue(), { value: cookieConsentString })];
|
||||||
|
spyOn(JSON, 'parse');
|
||||||
|
});
|
||||||
|
it('should return the cookie consents object', () => {
|
||||||
|
service.getSettingsForUser(user);
|
||||||
|
expect(JSON.parse).toHaveBeenCalledWith(cookieConsentString);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettingsForUser when there are changes', () => {
|
||||||
|
const cookieConsent = { test: 'testt' };
|
||||||
|
const cookieConsentString = '{test: \'testt\'}';
|
||||||
|
const operation = { op: 'add', path: 'metadata/dc.agreements.cookie', value: cookieConsentString };
|
||||||
|
let updatedUser;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
updatedUser = cloneDeep(user);
|
||||||
|
|
||||||
|
spyOn(updatedUser, 'setMetadata');
|
||||||
|
spyOn(JSON, 'stringify').and.returnValue(cookieConsentString);
|
||||||
|
ePersonService.createPatchFromCache.and.returnValue(observableOf([operation]))
|
||||||
|
});
|
||||||
|
it('should call patch on the data service', () => {
|
||||||
|
service.setSettingsForUser(updatedUser, cookieConsent);
|
||||||
|
expect(updatedUser.setMetadata).toHaveBeenCalledWith(COOKIE_MDFIELD, undefined, cookieConsentString);
|
||||||
|
expect(ePersonService.patch).toHaveBeenCalledWith(updatedUser, [operation])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettingsForUser when there are no changes', () => {
|
||||||
|
const cookieConsent = { test: 'testt' };
|
||||||
|
const cookieConsentString = '{test: \'testt\'}';
|
||||||
|
let updatedUser;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
updatedUser = cloneDeep(user);
|
||||||
|
|
||||||
|
spyOn(updatedUser, 'setMetadata');
|
||||||
|
spyOn(JSON, 'stringify').and.returnValue(cookieConsentString);
|
||||||
|
ePersonService.createPatchFromCache.and.returnValue(observableOf([]))
|
||||||
|
});
|
||||||
|
it('should not call patch on the data service', () => {
|
||||||
|
service.setSettingsForUser(updatedUser, cookieConsent);
|
||||||
|
expect(updatedUser.setMetadata).toHaveBeenCalledWith(COOKIE_MDFIELD, undefined, cookieConsentString);
|
||||||
|
expect(ePersonService.patch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
257
src/app/shared/cookies/browser-klaro.service.ts
Normal file
257
src/app/shared/cookies/browser-klaro.service.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import * as Klaro from 'klaro'
|
||||||
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { switchMap, take } from 'rxjs/operators';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { KlaroService } from './klaro.service';
|
||||||
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
|
import { CookieService } from '../../core/services/cookie.service';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { cloneDeep, debounce } from 'lodash';
|
||||||
|
import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata field to store a user's cookie consent preferences in
|
||||||
|
*/
|
||||||
|
export const COOKIE_MDFIELD = 'dspace.agreements.cookies';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix key for app title messages
|
||||||
|
*/
|
||||||
|
const cookieNameMessagePrefix = 'cookies.consent.app.title.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix key for app description messages
|
||||||
|
*/
|
||||||
|
const cookieDescriptionMessagePrefix = 'cookies.consent.app.description.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix key for app purpose messages
|
||||||
|
*/
|
||||||
|
const cookiePurposeMessagePrefix = 'cookies.consent.purpose.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update request debounce in ms
|
||||||
|
*/
|
||||||
|
const updateDebounce = 300;
|
||||||
|
/**
|
||||||
|
* Browser implementation for the KlaroService, representing a service for handling Klaro consent preferences and UI
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BrowserKlaroService extends KlaroService {
|
||||||
|
/**
|
||||||
|
* Initial Klaro configuration
|
||||||
|
*/
|
||||||
|
klaroConfig = klaroConfiguration;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private ePersonService: EPersonDataService,
|
||||||
|
private cookieService: CookieService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initializes the service:
|
||||||
|
* - Retrieves the current authenticated user
|
||||||
|
* - Checks if the translation service is ready
|
||||||
|
* - Initialize configuration for users
|
||||||
|
* - Add and translate klaro configuration messages
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
this.translateService.setDefaultLang(environment.defaultLanguage);
|
||||||
|
|
||||||
|
const user$: Observable<EPerson> = this.getUser$();
|
||||||
|
|
||||||
|
const translationServiceReady$ = this.translateService.get('loading.default').pipe(take(1));
|
||||||
|
|
||||||
|
observableCombineLatest(user$, translationServiceReady$)
|
||||||
|
.subscribe(([user, translation]: [EPerson, string]) => {
|
||||||
|
user = cloneDeep(user);
|
||||||
|
|
||||||
|
if (hasValue(user)) {
|
||||||
|
this.initializeUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all message keys for apps and purposes
|
||||||
|
*/
|
||||||
|
this.addAppMessages();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe on a message to make sure the translation service is ready
|
||||||
|
* Translate all keys in the translation section of the configuration
|
||||||
|
* Show the configuration if the configuration has not been confirmed
|
||||||
|
*/
|
||||||
|
this.translateConfiguration();
|
||||||
|
Klaro.renderKlaro(this.klaroConfig, false);
|
||||||
|
Klaro.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize configuration for the logged in user
|
||||||
|
* @param user The authenticated user
|
||||||
|
*/
|
||||||
|
private initializeUser(user: EPerson) {
|
||||||
|
this.klaroConfig.callback = debounce((consent, app) => this.updateSettingsForUsers(user), updateDebounce);
|
||||||
|
this.klaroConfig.storageName = this.getStorageName(user.uuid);
|
||||||
|
|
||||||
|
const anonCookie = this.cookieService.get(ANONYMOUS_STORAGE_NAME_KLARO);
|
||||||
|
if (hasValue(this.getSettingsForUser(user))) {
|
||||||
|
this.restoreSettingsForUsers(user);
|
||||||
|
} else if (hasValue(anonCookie)) {
|
||||||
|
this.cookieService.set(this.getStorageName(user.uuid), anonCookie);
|
||||||
|
this.updateSettingsForUsers(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the currently logged in user
|
||||||
|
* Returns undefined when no one is logged in
|
||||||
|
*/
|
||||||
|
private getUser$() {
|
||||||
|
return this.authService.isAuthenticated()
|
||||||
|
.pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((loggedIn: boolean) => {
|
||||||
|
if (loggedIn) {
|
||||||
|
return this.authService.getAuthenticatedUserFromStore();
|
||||||
|
}
|
||||||
|
return observableOf(undefined);
|
||||||
|
}),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a title translation key
|
||||||
|
* @param title
|
||||||
|
*/
|
||||||
|
private getTitleTranslation(title: string) {
|
||||||
|
return cookieNameMessagePrefix + title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a description translation key
|
||||||
|
* @param description
|
||||||
|
*/
|
||||||
|
private getDescriptionTranslation(description: string) {
|
||||||
|
return cookieDescriptionMessagePrefix + description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a purpose translation key
|
||||||
|
* @param purpose
|
||||||
|
*/
|
||||||
|
private getPurposeTranslation(purpose: string) {
|
||||||
|
return cookiePurposeMessagePrefix + purpose;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the cookie consent form
|
||||||
|
*/
|
||||||
|
showSettings() {
|
||||||
|
Klaro.show(this.klaroConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add message keys for all apps and purposes
|
||||||
|
*/
|
||||||
|
addAppMessages() {
|
||||||
|
this.klaroConfig.apps.forEach((app) => {
|
||||||
|
this.klaroConfig.translations.en[app.name] = { title: this.getTitleTranslation(app.name), description: this.getDescriptionTranslation(app.name) };
|
||||||
|
app.purposes.forEach((purpose) => {
|
||||||
|
this.klaroConfig.translations.en.purposes[purpose] = this.getPurposeTranslation(purpose);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the translation section from the Klaro configuration
|
||||||
|
*/
|
||||||
|
translateConfiguration() {
|
||||||
|
/**
|
||||||
|
* Make sure the fallback language is english
|
||||||
|
*/
|
||||||
|
this.translateService.setDefaultLang(environment.defaultLanguage);
|
||||||
|
|
||||||
|
this.translate(this.klaroConfig.translations.en);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate string values in an object
|
||||||
|
* @param object The object containing translation keys
|
||||||
|
*/
|
||||||
|
private translate(object) {
|
||||||
|
if (typeof (object) === 'string') {
|
||||||
|
return this.translateService.instant(object);
|
||||||
|
}
|
||||||
|
Object.entries(object).forEach(([key, value]: [string, any]) => {
|
||||||
|
object[key] = this.translate(value);
|
||||||
|
});
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the stored Klaro consent settings for a user
|
||||||
|
* @param user The user to resolve the consent for
|
||||||
|
*/
|
||||||
|
getSettingsForUser(user: EPerson) {
|
||||||
|
const mdValue = user.firstMetadataValue(COOKIE_MDFIELD);
|
||||||
|
return hasValue(mdValue) ? JSON.parse(mdValue) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the Klaro consent settings for a user in a metadata field
|
||||||
|
* @param user The user to save the settings for
|
||||||
|
* @param config The consent settings for the user to save
|
||||||
|
*/
|
||||||
|
setSettingsForUser(user: EPerson, config: object) {
|
||||||
|
if (isNotEmpty(config)) {
|
||||||
|
user.setMetadata(COOKIE_MDFIELD, undefined, JSON.stringify(config));
|
||||||
|
} else {
|
||||||
|
user.removeMetadata(COOKIE_MDFIELD);
|
||||||
|
}
|
||||||
|
this.ePersonService.createPatchFromCache(user)
|
||||||
|
.pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((operations: Operation[]) => {
|
||||||
|
if (isNotEmpty(operations)) {
|
||||||
|
return this.ePersonService.patch(user, operations)
|
||||||
|
}
|
||||||
|
return observableOf(undefined)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the users consent settings cookie based on the user's stored consent settings
|
||||||
|
* @param user The user to save the settings for
|
||||||
|
*/
|
||||||
|
restoreSettingsForUsers(user: EPerson) {
|
||||||
|
this.cookieService.set(this.getStorageName(user.uuid), this.getSettingsForUser(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the consent settings for a user based on the current cookie for this user
|
||||||
|
* @param user
|
||||||
|
*/
|
||||||
|
updateSettingsForUsers(user: EPerson) {
|
||||||
|
this.setSettingsForUser(user, this.cookieService.get(this.getStorageName(user.uuid)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the storage name for klaro cookies based on the user's identifier
|
||||||
|
* @param identifier The user's uuid
|
||||||
|
*/
|
||||||
|
getStorageName(identifier: string) {
|
||||||
|
return 'klaro-' + identifier
|
||||||
|
}
|
||||||
|
}
|
159
src/app/shared/cookies/klaro-configuration.ts
Normal file
159
src/app/shared/cookies/klaro-configuration.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
|
||||||
|
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
|
||||||
|
import { LANG_COOKIE } from '../../core/locale/locale.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cookie for has_agreed_end_user
|
||||||
|
*/
|
||||||
|
export const HAS_AGREED_END_USER = 'dsHasAgreedEndUser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage name used to store klaro cookie
|
||||||
|
*/
|
||||||
|
export const ANONYMOUS_STORAGE_NAME_KLARO = 'klaro-anonymous';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Klaro configuration
|
||||||
|
* For more information see https://kiprotect.com/docs/klaro/annotated-config
|
||||||
|
*/
|
||||||
|
export const klaroConfiguration: any = {
|
||||||
|
storageName: ANONYMOUS_STORAGE_NAME_KLARO,
|
||||||
|
|
||||||
|
privacyPolicy: '/info/privacy',
|
||||||
|
|
||||||
|
/*
|
||||||
|
Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in
|
||||||
|
the consent notice. We strongly advise against using this under most
|
||||||
|
circumstances, as it keeps the user from customizing his/her consent choices.
|
||||||
|
*/
|
||||||
|
hideLearnMore: false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
Setting 'acceptAll' to 'true' will show an "accept all" button in the notice and
|
||||||
|
modal, which will enable all third-party apps if the user clicks on it. If set
|
||||||
|
to 'false', there will be an "accept" button that will only enable the apps that
|
||||||
|
are enabled in the consent modal.
|
||||||
|
*/
|
||||||
|
acceptAll: true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can also set a custom expiration time for the Klaro cookie. By default, it
|
||||||
|
will expire after 30 days. Only relevant if 'storageMethod' is set to 'cookie'.
|
||||||
|
*/
|
||||||
|
cookieExpiresAfterDays: 365,
|
||||||
|
|
||||||
|
htmlTexts: true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can overwrite existing translations and add translations for your app
|
||||||
|
descriptions and purposes. See `src/translations/` for a full list of
|
||||||
|
translations that can be overwritten:
|
||||||
|
https://github.com/KIProtect/klaro/tree/master/src/translations
|
||||||
|
*/
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
acceptAll: 'cookies.consent.accept-all',
|
||||||
|
acceptSelected: 'cookies.consent.accept-selected',
|
||||||
|
app: {
|
||||||
|
optOut: {
|
||||||
|
description: 'cookies.consent.app.opt-out.description',
|
||||||
|
title: 'cookies.consent.app.opt-out.title'
|
||||||
|
},
|
||||||
|
purpose: 'cookies.consent.app.purpose',
|
||||||
|
purposes: 'cookies.consent.app.purposes',
|
||||||
|
required: {
|
||||||
|
description: 'cookies.consent.app.required.description',
|
||||||
|
title: 'cookies.consent.app.required.title'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: 'cookies.consent.close',
|
||||||
|
decline: 'cookies.consent.decline',
|
||||||
|
changeDescription: 'cookies.consent.update',
|
||||||
|
consentNotice: {
|
||||||
|
description: 'cookies.consent.content-notice.description',
|
||||||
|
learnMore: 'cookies.consent.content-notice.learnMore'
|
||||||
|
},
|
||||||
|
consentModal: {
|
||||||
|
description: 'cookies.consent.content-modal.description',
|
||||||
|
privacyPolicy: {
|
||||||
|
name: 'cookies.consent.content-modal.privacy-policy.name',
|
||||||
|
text: 'cookies.consent.content-modal.privacy-policy.text'
|
||||||
|
},
|
||||||
|
title: 'cookies.consent.content-modal.title'
|
||||||
|
},
|
||||||
|
purposes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'authentication',
|
||||||
|
purposes: ['functional'],
|
||||||
|
required: true,
|
||||||
|
cookies: [
|
||||||
|
TOKENITEM,
|
||||||
|
IMPERSONATING_COOKIE,
|
||||||
|
REDIRECT_COOKIE
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preferences',
|
||||||
|
purposes: ['functional'],
|
||||||
|
required: true,
|
||||||
|
cookies: [
|
||||||
|
LANG_COOKIE
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'acknowledgement',
|
||||||
|
purposes: ['functional'],
|
||||||
|
required: true,
|
||||||
|
cookies: [
|
||||||
|
[/^klaro-.+$/],
|
||||||
|
HAS_AGREED_END_USER
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'google-analytics',
|
||||||
|
purposes: ['statistical'],
|
||||||
|
required: false,
|
||||||
|
cookies: [
|
||||||
|
// /*
|
||||||
|
// you an either only provide a cookie name or regular expression (regex) or a list
|
||||||
|
// consisting of a name or regex, a path and a cookie domain. Providing a path and
|
||||||
|
// domain is necessary if you have apps that set cookies for a path that is not
|
||||||
|
// "/", or a domain that is not the current domain. If you do not set these values
|
||||||
|
// properly, the cookie can't be deleted by Klaro, as there is no way to access the
|
||||||
|
// path or domain of a cookie in JS. Notice that it is not possible to delete
|
||||||
|
// cookies that were set on a third-party domain, or cookies that have the HTTPOnly
|
||||||
|
// attribute: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#new-
|
||||||
|
// cookie_domain
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// This rule will match cookies that contain the string '_pk_' and that are set on
|
||||||
|
// the path '/' and the domain 'klaro.kiprotect.com'
|
||||||
|
// */
|
||||||
|
[/^_ga.?$/],
|
||||||
|
[/^_gid$/],
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// Same as above, only for the 'localhost' domain
|
||||||
|
// */
|
||||||
|
// [/^_pk_.*$/, '/', 'localhost'],
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// This rule will match all cookies named 'piwik_ignore' that are set on the path
|
||||||
|
// '/' on the current domain
|
||||||
|
// */
|
||||||
|
// 'piwik_ignore',
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
If 'onlyOnce' is set to 'true', the app will only be executed once regardless
|
||||||
|
how often the user toggles it on and off. This is relevant e.g. for tracking
|
||||||
|
scripts that would generate new page view events every time Klaro disables and
|
||||||
|
re-enables them due to a consent change by the user.
|
||||||
|
*/
|
||||||
|
onlyOnce: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
17
src/app/shared/cookies/klaro.service.ts
Normal file
17
src/app/shared/cookies/klaro.service.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class representing a service for handling Klaro consent preferences and UI
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export abstract class KlaroService {
|
||||||
|
/**
|
||||||
|
* Initializes the service
|
||||||
|
*/
|
||||||
|
abstract initialize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a the dialog with the current consent preferences
|
||||||
|
*/
|
||||||
|
abstract showSettings();
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
export function getMockTranslateService(): TranslateService {
|
export function getMockTranslateService(): TranslateService {
|
||||||
return jasmine.createSpyObj('translateService', {
|
return jasmine.createSpyObj('translateService', {
|
||||||
get: jasmine.createSpy('get'),
|
get: jasmine.createSpy('get'),
|
||||||
instant: jasmine.createSpy('instant')
|
instant: jasmine.createSpy('instant'),
|
||||||
|
setDefaultLang: jasmine.createSpy('setDefaultLang')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -937,6 +937,68 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"cookies.consent.accept-all": "Accept all",
|
||||||
|
|
||||||
|
"cookies.consent.accept-selected": "Accept selected",
|
||||||
|
|
||||||
|
"cookies.consent.app.opt-out.description": "This app is loaded by default (but you can opt out)",
|
||||||
|
|
||||||
|
"cookies.consent.app.opt-out.title": "(opt-out)",
|
||||||
|
|
||||||
|
"cookies.consent.app.purpose": "purpose",
|
||||||
|
|
||||||
|
"cookies.consent.app.required.description": "This application is always required",
|
||||||
|
|
||||||
|
"cookies.consent.app.required.title": "(always required)",
|
||||||
|
|
||||||
|
"cookies.consent.update": "There were changes since your last visit, please update your consent.",
|
||||||
|
|
||||||
|
"cookies.consent.close": "Close",
|
||||||
|
|
||||||
|
"cookies.consent.decline": "Decline",
|
||||||
|
|
||||||
|
"cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>. <br/> To learn more, please read our {privacyPolicy}.",
|
||||||
|
|
||||||
|
"cookies.consent.content-notice.learnMore": "Customize",
|
||||||
|
|
||||||
|
"cookies.consent.content-modal.description": "Here you can see and customize the information that we collect about you.",
|
||||||
|
|
||||||
|
"cookies.consent.content-modal.privacy-policy.name": "privacy policy",
|
||||||
|
|
||||||
|
"cookies.consent.content-modal.privacy-policy.text": "To learn more, please read our {privacyPolicy}.",
|
||||||
|
|
||||||
|
"cookies.consent.content-modal.title": "Information that we collect",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"cookies.consent.app.title.authentication": "Authentication",
|
||||||
|
|
||||||
|
"cookies.consent.app.description.authentication": "Required for signing you in",
|
||||||
|
|
||||||
|
|
||||||
|
"cookies.consent.app.title.preferences": "Preferences",
|
||||||
|
|
||||||
|
"cookies.consent.app.description.preferences": "Required for saving your preferences",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"cookies.consent.app.title.acknowledgement": "Acknowledgement",
|
||||||
|
|
||||||
|
"cookies.consent.app.description.acknowledgement": "Required for saving your acknowledgements and consents",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"cookies.consent.app.title.google-analytics": "Google Analytics",
|
||||||
|
|
||||||
|
"cookies.consent.app.description.google-analytics": "Allows us to track statistical data",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"cookies.consent.purpose.functional": "Functional",
|
||||||
|
|
||||||
|
"cookies.consent.purpose.statistical": "Statistical",
|
||||||
|
|
||||||
|
|
||||||
"curation-task.task.checklinks.label": "Check Links in Metadata",
|
"curation-task.task.checklinks.label": "Check Links in Metadata",
|
||||||
|
|
||||||
"curation-task.task.noop.label": "NOOP",
|
"curation-task.task.noop.label": "NOOP",
|
||||||
@@ -1051,6 +1113,13 @@
|
|||||||
|
|
||||||
"footer.link.duraspace": "DuraSpace",
|
"footer.link.duraspace": "DuraSpace",
|
||||||
|
|
||||||
|
"footer.link.cookies": "Cookie settings",
|
||||||
|
|
||||||
|
"footer.link.privacy-policy": "Privacy policy",
|
||||||
|
|
||||||
|
"footer.link.end-user-agreement":"End User Agreement",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"forgot-email.form.header": "Forgot Password",
|
"forgot-email.form.header": "Forgot Password",
|
||||||
|
|
||||||
|
@@ -21,6 +21,8 @@ import { AuthService } from '../../app/core/auth/auth.service';
|
|||||||
import { Angulartics2RouterlessModule } from 'angulartics2/routerlessmodule';
|
import { Angulartics2RouterlessModule } from 'angulartics2/routerlessmodule';
|
||||||
import { SubmissionService } from '../../app/submission/submission.service';
|
import { SubmissionService } from '../../app/submission/submission.service';
|
||||||
import { StatisticsModule } from '../../app/statistics/statistics.module';
|
import { StatisticsModule } from '../../app/statistics/statistics.module';
|
||||||
|
import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service';
|
||||||
|
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||||
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
||||||
import {
|
import {
|
||||||
BrowserHardRedirectService,
|
BrowserHardRedirectService,
|
||||||
@@ -80,6 +82,10 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
provide: CookieService,
|
provide: CookieService,
|
||||||
useClass: ClientCookieService
|
useClass: ClientCookieService
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: KlaroService,
|
||||||
|
useClass: BrowserKlaroService
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: SubmissionService,
|
provide: SubmissionService,
|
||||||
useClass: SubmissionService
|
useClass: SubmissionService
|
||||||
|
@@ -25,7 +25,8 @@ module.exports = {
|
|||||||
module: {
|
module: {
|
||||||
noParse: /polyfills-.*\.js/,
|
noParse: /polyfills-.*\.js/,
|
||||||
rules: [
|
rules: [
|
||||||
{ test: /\.ts$/, loader: 'ts-loader',
|
{
|
||||||
|
test: /\.ts$/, loader: 'ts-loader',
|
||||||
options: {
|
options: {
|
||||||
configFile: "tsconfig.server.json"
|
configFile: "tsconfig.server.json"
|
||||||
} },
|
} },
|
||||||
|
@@ -6211,6 +6211,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||||
|
|
||||||
|
klaro@^0.6.3:
|
||||||
|
version "0.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/klaro/-/klaro-0.6.3.tgz#b2aaa810d17f073c9a1b5eab618a9f13d5f40fa3"
|
||||||
|
integrity sha512-rRP37FaJaHHSScHIe3YUdMZJ1asxOF5+C/RMrFB2RzhAUfGVMM5/GiucECM3Si1lhW2LL0xGVymE8JhYZl2Bjg==
|
||||||
|
|
||||||
last-call-webpack-plugin@^3.0.0:
|
last-call-webpack-plugin@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
|
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
|
||||||
|
Reference in New Issue
Block a user