From b3291453d8bb3c14af43b998c44f4c5af7d311a9 Mon Sep 17 00:00:00 2001 From: Lennard Strohmeyer <lennard.strohmeyer@digitallearning.gmbh> Date: Tue, 21 Jan 2025 15:00:26 +0100 Subject: [PATCH] #109 implemented, cleanup and small fixes --- .../migrations/0004_userconsents_active.py | 18 +++ src/consents/models.py | 1 + src/consents/urls.py | 1 + src/consents/views.py | 115 +++++++++++++- src/frontend/src/app/app-routing.module.ts | 37 +++-- src/frontend/src/app/app.module.ts | 101 ++++++------ .../consent-history.component.html | 23 +++ .../consent-history.component.scss} | 0 .../consent-history.component.spec.ts | 23 +++ .../consent-history.component.ts | 19 +++ .../consent-management/consentDeclaration.ts | 148 +++++++++--------- .../provider-setting.component.html | 5 +- .../provider-setting.component.ts | 1 + .../navigation/header/header.component.html | 17 +- .../app/navigation/header/header.component.ts | 2 + src/frontend/src/app/services/api.service.ts | 17 ++ .../src/environments/environment.prod.ts | 8 +- src/frontend/src/environments/environment.ts | 8 +- src/frontend/src/favicon.ico | Bin 948 -> 0 bytes src/frontend/src/locale/messages.de.xlf | 11 +- src/frontend/src/locale/messages.xlf | 11 +- 21 files changed, 411 insertions(+), 155 deletions(-) create mode 100644 src/consents/migrations/0004_userconsents_active.py create mode 100644 src/frontend/src/app/consent-management/consent-history/consent-history.component.html rename src/frontend/src/{styles.scss => app/consent-management/consent-history/consent-history.component.scss} (100%) create mode 100644 src/frontend/src/app/consent-management/consent-history/consent-history.component.spec.ts create mode 100644 src/frontend/src/app/consent-management/consent-history/consent-history.component.ts delete mode 100644 src/frontend/src/favicon.ico diff --git a/src/consents/migrations/0004_userconsents_active.py b/src/consents/migrations/0004_userconsents_active.py new file mode 100644 index 0000000..c043ef4 --- /dev/null +++ b/src/consents/migrations/0004_userconsents_active.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.2 on 2025-01-21 13:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('consents', '0003_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userconsents', + name='active', + field=models.BooleanField(default=True), + ), + ] diff --git a/src/consents/models.py b/src/consents/models.py index d26eae4..37761ed 100644 --- a/src/consents/models.py +++ b/src/consents/models.py @@ -15,6 +15,7 @@ class UserConsents(models.Model): object = models.JSONField() updated = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) + active = models.BooleanField(default=True) def __str__(self): return "User consent: " + self.verb diff --git a/src/consents/urls.py b/src/consents/urls.py index 790b358..54c1ed5 100644 --- a/src/consents/urls.py +++ b/src/consents/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('user/analytics-tokens', views.GetUserConsentAnalyticsTokens.as_view()), path('user/analytics-tokens/consent', views.SaveUserConsentAnalyticsTokens.as_view()), path('user/providers', views.GetUserConsentProviders.as_view()), + path('user/history', views.GetUserConsentHistoryView.as_view()), path('user/<provider_id>', views.GetUserConsentView.as_view()), path('user/status/<user_id>/third-party', views.GetUsersConsentsThirdPartyView.as_view()), path('user/save/third-party', views.SaveUserConsentThirdPartyView.as_view()), diff --git a/src/consents/views.py b/src/consents/views.py index 680047a..51b1807 100644 --- a/src/consents/views.py +++ b/src/consents/views.py @@ -4,6 +4,7 @@ import random import os import secrets import time +from datetime import timedelta, datetime from django.core.cache import cache from django.conf import settings @@ -80,7 +81,7 @@ def get_user_consent(user, provider_id): """ Get all user consents for a given provider and merge them in a provider schema. """ - user_consents = UserConsents.objects.filter(user=user, provider__pk=provider_id) + user_consents = UserConsents.objects.filter(user=user, provider__pk=provider_id, active=True) if user_consents.first(): provider_schema = user_consents.first().provider_schema @@ -93,15 +94,15 @@ def get_user_consent(user, provider_id): def save_user_consent(user, provider_schema, verbs): """ - Deletes existing user consents for user and provider. Afterwards, new user consents are created. + Inactivates existing user consents for user and provider. Afterwards, new user consents are created. """ old_user_consents = UserConsents.objects.filter( user=user, provider=provider_schema.provider.id - ) - old_user_consents.delete() + ).update(active=False) + for verb in verbs: try: - UserConsents.objects.get(user=user, verb=verb["id"], provider=provider_schema.provider.id) + UserConsents.objects.get(user=user, verb=verb["id"], provider=provider_schema.provider.id, active=True) # user consent for verb already exists, skip except UserConsents.DoesNotExist: provider = Provider.objects.get(pk=provider_schema.provider.id) @@ -160,6 +161,44 @@ def validate_distinct_group_ids(provider_schema): raise ValueError("Provider schema contains ambiguous group ids.") +def group_consents_by_date(consents): + """ + Groups consent declaration by provider and date (windows size 1 minute) + to identify those likely created within the same action. + """ + # Ensure objects are sorted by their created timestamp + consents = sorted(consents, key=lambda con: con.created) + + groups = [] # To hold the result + current_group = {"created": None, "consents": {}} + + for obj in consents: + if current_group["created"] is None: + current_group["created"] = obj.created.replace(tzinfo=None) + if obj.provider.id not in current_group["consents"].keys(): + current_group["consents"][obj.provider.id] = [obj] + else: + # Check if the object can be added to the current group + time_diff = obj.created.replace(tzinfo=None) - current_group["created"] + if time_diff <= timedelta(minutes=1): + current_group["consents"][obj.provider.id].append(obj) + # Recalculate the average timestamp for the current group + all_current_consents = [] + for _, consents in current_group["consents"].items(): + all_current_consents += consents + total_time = sum((o.created.timestamp() for o in all_current_consents), 0) + avg_time = total_time / sum([len(subgroup) for _, subgroup in current_group["consents"].items()]) + current_group["created"] = datetime.fromtimestamp(avg_time) + else: + # Finalize the current group and start a new one + groups.append(current_group) + current_group = {"created": obj.created.replace(tzinfo=None), "consents": {obj.provider.id: [obj]}} + + if len(current_group["consents"]) > 0: + groups.append(current_group) + + return groups + class GetProviderSchemasView(generics.ListAPIView): """ Lists all providers including schema history. @@ -342,6 +381,70 @@ class GetUserConsentView(APIView): ) +class GetUserConsentHistoryView(APIView): + """ + This endpoint returns all consents declared by the user, sorted by creation date. + """ + + permission_classes = (IsAuthenticated,) + + def get(self, request): + user = request.user + try: + user_consents = UserConsents.objects.filter(user=user).order_by('created') + + if not user_consents.first(): + return JsonResponse( + { + "consents": [], + }, + safe=False, + status=status.HTTP_200_OK, + ) + else: + consents_grouped = group_consents_by_date(consents=user_consents) + consents_mapped = [] + for consent_group in consents_grouped: + consent_group_mapped = { + "created": consent_group["created"], + "consents": [] + } + for provider_id, consents in consent_group["consents"].items(): + provider_schema = consents[0].provider_schema + user_consent = { + "id": provider_schema.provider.id, + "name": provider_schema.provider.name, + "description": provider_schema.provider.description, + "groups": provider_schema.groups, + "essential_verbs": provider_schema.essential_verbs, + "created": consent_group["created"], + } + for group in user_consent.get("groups"): + for verb in group.get("verbs", []): + user_verb = find_user_verb(verb.get("id"), [consent.__dict__ for consent in consents]) + if user_verb: + verb.update({"consented": user_verb.get("consented")}) + verb.update({"objects": json.loads(user_verb.get("object"))}) + consent_group_mapped["consents"].append(user_consent) + consents_mapped.append(consent_group_mapped) + + return JsonResponse( + { + "groups": consents_mapped + }, + safe=False, + status=status.HTTP_200_OK, + ) + except ObjectDoesNotExist: + return JsonResponse( + { + "message": "User has no consent declaration record.", + "no_consent_record": True, + }, + safe=False, + status=status.HTTP_400_BAD_REQUEST, + ) + class CreateUserConsentView(APIView): def post(self, request): @@ -499,7 +602,7 @@ class SaveUserConsentAnalyticsTokens(APIView): ) verbs = AnalyticsTokenVerb.objects.filter(analytics_token_id = token.id) for verb in verbs: - consent = UserConsents.objects.filter(verb = verb.verb, user_id=user.id, provider_id = verb.provider).first() + consent = UserConsents.objects.filter(verb = verb.verb, user_id=user.id, provider_id = verb.provider, active=True).first() consent.consented = True consent.save() diff --git a/src/frontend/src/app/app-routing.module.ts b/src/frontend/src/app/app-routing.module.ts index 802ef98..3d57b7b 100644 --- a/src/frontend/src/app/app-routing.module.ts +++ b/src/frontend/src/app/app-routing.module.ts @@ -14,26 +14,29 @@ import { DataRemovalComponent } from './data-removal/data-removal.component' import { ApplicationTokensComponent } from './consent-management/application-tokens/application-tokens.component' import { AnalyticsTokensComponent } from './consent-management/analytics-tokens/analytics-tokens.component' import { AnalyticsEngineComponent } from './analytics-engine/analytics-engine.component' +import { ConsentHistoryComponent } from './consent-management/consent-history/consent-history.component' const routes: Routes = [ - { path: 'home', component: HomeComponent }, - { path: 'consent-management', component: ConsentManagementComponent, canActivate: [AuthGuard] }, - { path: 'merge-actors', component: MergeDataComponent, canActivate: [AuthGuard] }, - { path: 'analytics-engines', component: AnalyticsEngineComponent, canActivate: [AuthGuard] }, - { path: 'login', component: LoginPageComponent }, - { path: 'provider', component: ProviderComponent, canActivate: [AuthGuard] }, - { path: 'application-tokens', component: ApplicationTokensComponent, canActivate: [AuthGuard] }, - { path: 'analytics-tokens', component: AnalyticsTokensComponent, canActivate: [AuthGuard] }, - { path: 'profil', component: UserProfilComponent, canActivate: [AuthGuard] }, - { path: 'data-disclosure', component: DataDisclosureComponent, canActivate: [AuthGuard] }, - { path: 'data-removal', component: DataRemovalComponent, canActivate: [AuthGuard] }, - { path: 'legal-notice', component: LegalNoticeComponent }, - { path: '', redirectTo: '/home', pathMatch: 'full' }, - { path: '**', component: PageNotFoundComponent } + { path: 'home', component: HomeComponent }, + { path: 'consent-management', component: ConsentManagementComponent, canActivate: [AuthGuard] }, + { path: 'consent-history', component: ConsentHistoryComponent, canActivate: [AuthGuard] }, + { path: 'merge-actors', component: MergeDataComponent, canActivate: [AuthGuard] }, + { path: 'analytics-engines', component: AnalyticsEngineComponent, canActivate: [AuthGuard] }, + { path: 'login', component: LoginPageComponent }, + { path: 'provider', component: ProviderComponent, canActivate: [AuthGuard] }, + { path: 'application-tokens', component: ApplicationTokensComponent, canActivate: [AuthGuard] }, + { path: 'analytics-tokens', component: AnalyticsTokensComponent, canActivate: [AuthGuard] }, + { path: 'profil', component: UserProfilComponent, canActivate: [AuthGuard] }, + { path: 'data-disclosure', component: DataDisclosureComponent, canActivate: [AuthGuard] }, + { path: 'data-removal', component: DataRemovalComponent, canActivate: [AuthGuard] }, + { path: 'legal-notice', component: LegalNoticeComponent }, + { path: '', redirectTo: '/home', pathMatch: 'full' }, + { path: '**', component: PageNotFoundComponent } ] @NgModule({ - imports: [RouterModule.forRoot(routes, { useHash: true })], - exports: [RouterModule] + imports: [RouterModule.forRoot(routes, { useHash: true })], + exports: [RouterModule] }) -export class AppRoutingModule {} +export class AppRoutingModule { +} diff --git a/src/frontend/src/app/app.module.ts b/src/frontend/src/app/app.module.ts index 7a8cbfc..fab3a63 100644 --- a/src/frontend/src/app/app.module.ts +++ b/src/frontend/src/app/app.module.ts @@ -7,7 +7,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { LayoutModule } from '@angular/cdk/layout' import { HeaderComponent } from './navigation/header/header.component' import { - ConsentManagementComponent, + ConsentManagementComponent } from './consent-management/consent-management.component' import { PageNotFoundComponent } from './page-not-found/page-not-found.component' import { LoginPageComponent } from './login-page/login-page.component' @@ -16,7 +16,7 @@ import { FooterComponent } from './navigation/footer/footer.component' import { LegalNoticeComponent } from './legal-notice/legal-notice.component' import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http' import { - PrivacySettingComponent + PrivacySettingComponent } from './consent-management/provider-settings/provider-setting.component' import { HttpRequestInterceptor } from './interceptors/http-request-interceptor' import { JwtInterceptor } from './interceptors/jwt.interceptor' @@ -35,14 +35,14 @@ import { AnalyticsTokensComponent } from './consent-management/analytics-tokens/ import { SchemaChangeComponent } from './consent-management/schema-change/schema-change.component' import { ObjectChangesComponent } from './consent-management/schema-change/object-changes/object-changes.component' import { CreateTokenDialog } from './dialogs/create-token-dialog/create-token-dialog' -import { OrderByPipe } from './order-by.pipe'; +import { OrderByPipe } from './order-by.pipe' import { MergeDataComponent } from './merge-data/merge-data.component' import { AnalyticsEngineComponent } from './analytics-engine/analytics-engine.component' -import { AnalysisCard } from './analytics-engine/analysis-card/analysis-card.component'; -import { NZ_I18N } from 'ng-zorro-antd/i18n'; -import { de_DE } from 'ng-zorro-antd/i18n'; +import { AnalysisCard } from './analytics-engine/analysis-card/analysis-card.component' +import { NZ_I18N } from 'ng-zorro-antd/i18n' +import { de_DE } from 'ng-zorro-antd/i18n' import { NgOptimizedImage, registerLocaleData } from '@angular/common' -import de from '@angular/common/locales/de'; +import de from '@angular/common/locales/de' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { NzTabsModule } from 'ng-zorro-antd/tabs' import { NzGridModule } from 'ng-zorro-antd/grid' @@ -71,36 +71,38 @@ import { NzAlertModule } from 'ng-zorro-antd/alert' import { NzMessageService } from 'ng-zorro-antd/message' import { NzSpinModule } from 'ng-zorro-antd/spin' import { NzDatePickerModule } from 'ng-zorro-antd/date-picker' +import { ConsentHistoryComponent } from './consent-management/consent-history/consent-history.component' -registerLocaleData(de); +registerLocaleData(de) @NgModule({ - declarations: [ - AppComponent, - HeaderComponent, - ConsentManagementComponent, - PageNotFoundComponent, - LoginPageComponent, - WizardDialog, - FooterComponent, - LegalNoticeComponent, - PrivacySettingComponent, - HomeComponent, - ProviderComponent, - UserProfilComponent, - DataDisclosureComponent, - DataRemovalComponent, - DeleteDialog, - ApplicationTokensComponent, - AnalyticsTokensComponent, - SchemaChangeComponent, - ObjectChangesComponent, - CreateTokenDialog, - OrderByPipe, - MergeDataComponent, - AnalysisCard, - AnalyticsEngineComponent - ], + declarations: [ + AppComponent, + HeaderComponent, + ConsentManagementComponent, + ConsentHistoryComponent, + PageNotFoundComponent, + LoginPageComponent, + WizardDialog, + FooterComponent, + LegalNoticeComponent, + PrivacySettingComponent, + HomeComponent, + ProviderComponent, + UserProfilComponent, + DataDisclosureComponent, + DataRemovalComponent, + DeleteDialog, + ApplicationTokensComponent, + AnalyticsTokensComponent, + SchemaChangeComponent, + ObjectChangesComponent, + CreateTokenDialog, + OrderByPipe, + MergeDataComponent, + AnalysisCard, + AnalyticsEngineComponent + ], imports: [ BrowserModule, AppRoutingModule, @@ -137,19 +139,20 @@ registerLocaleData(de); NzSpinModule, NzDatePickerModule ], - providers: [ - { - provide: APP_INITIALIZER, - useFactory: appInitializer, - multi: true, - deps: [AuthService, LocalStorageService, ThemeService] - }, - { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true }, - { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, - { provide: NZ_I18N, useValue: de_DE }, - NzModalService, - NzMessageService - ], - bootstrap: [AppComponent] + providers: [ + { + provide: APP_INITIALIZER, + useFactory: appInitializer, + multi: true, + deps: [AuthService, LocalStorageService, ThemeService] + }, + { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, + { provide: NZ_I18N, useValue: de_DE }, + NzModalService, + NzMessageService + ], + bootstrap: [AppComponent] }) -export class AppModule {} +export class AppModule { +} diff --git a/src/frontend/src/app/consent-management/consent-history/consent-history.component.html b/src/frontend/src/app/consent-management/consent-history/consent-history.component.html new file mode 100644 index 0000000..52fc28a --- /dev/null +++ b/src/frontend/src/app/consent-management/consent-history/consent-history.component.html @@ -0,0 +1,23 @@ +<nz-card style='margin: 8px;' [nzTitle]="cardTitle"> + <ng-template #cardTitle> + <h2 i18n="Consent History | Header entry @@consentHistoryHeader">Consent History</h2> + </ng-template> + <nz-collapse> + <nz-collapse-panel *ngFor="let consent of consentHistory" [nzHeader]="dateHeader"> + <ng-template #dateHeader> + {{ consent.created | date }} + </ng-template> + <nz-card + *ngFor="let userConsent of consent.consents" + [nzTitle]="userConsent.description" + > + <app-provider-setting + [preview]="true" + [consentDeclaration]="userConsent" + [previousUserConsent]="null" + ></app-provider-setting> + </nz-card> + + </nz-collapse-panel> + </nz-collapse> +</nz-card> diff --git a/src/frontend/src/styles.scss b/src/frontend/src/app/consent-management/consent-history/consent-history.component.scss similarity index 100% rename from src/frontend/src/styles.scss rename to src/frontend/src/app/consent-management/consent-history/consent-history.component.scss diff --git a/src/frontend/src/app/consent-management/consent-history/consent-history.component.spec.ts b/src/frontend/src/app/consent-management/consent-history/consent-history.component.spec.ts new file mode 100644 index 0000000..99709c7 --- /dev/null +++ b/src/frontend/src/app/consent-management/consent-history/consent-history.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConsentHistoryComponent } from './consent-history.component'; + +describe('ConsentHistoryComponent', () => { + let component: ConsentHistoryComponent; + let fixture: ComponentFixture<ConsentHistoryComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConsentHistoryComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConsentHistoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/src/app/consent-management/consent-history/consent-history.component.ts b/src/frontend/src/app/consent-management/consent-history/consent-history.component.ts new file mode 100644 index 0000000..a06ad41 --- /dev/null +++ b/src/frontend/src/app/consent-management/consent-history/consent-history.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core' +import { UserConsent } from '../consentDeclaration' +import { ApiService, GroupedConsentHistory } from '../../services/api.service' + + + +@Component({ + selector: 'consent-history', + templateUrl: 'consent-history.component.html', + styleUrls: ['./consent-history.component.scss'] +}) +export class ConsentHistoryComponent { + protected consentHistory: GroupedConsentHistory[] = []; + constructor(_apiService: ApiService) { + _apiService.getUserConsentHistory().subscribe(history => { + this.consentHistory = history.groups + }) + } +} diff --git a/src/frontend/src/app/consent-management/consentDeclaration.ts b/src/frontend/src/app/consent-management/consentDeclaration.ts index 369df5c..51e06e7 100644 --- a/src/frontend/src/app/consent-management/consentDeclaration.ts +++ b/src/frontend/src/app/consent-management/consentDeclaration.ts @@ -1,98 +1,100 @@ export interface XApiObjectSchema { - definition: Object - consented?: boolean - defaultConsent: boolean - label: string - id: string + definition: Object + consented?: boolean + defaultConsent: boolean + label: string + id: string } export interface XApiObjectConsented { - definition: Object - consented: boolean - defaultConsent: boolean - label: string - id: string + definition: Object + consented: boolean + defaultConsent: boolean + label: string + id: string } -interface XApiVerb {} +interface XApiVerb { +} export interface XApiVerbSchema { - id: string - label: string - description: string - defaultConsent: boolean - objects: XApiObjectSchema[] + id: string + label: string + description: string + defaultConsent: boolean + objects: XApiObjectSchema[] } export interface XApiVerbConsented extends XApiVerbSchema { - consented: boolean - objects: XApiObjectConsented[] + consented: boolean + objects: XApiObjectConsented[] } export interface ConsentGroupSchema { - id: string - label: string - description: string - purposeOfCollection: string - showVerbDetails: boolean - verbs: XApiVerbSchema[] - isDefault: boolean + id: string + label: string + description: string + purposeOfCollection: string + showVerbDetails: boolean + verbs: XApiVerbSchema[] + isDefault: boolean } export const instanceOfXApiVerbConsented = (object: any): object is XApiObjectConsented => { - return 'consented' in object + return 'consented' in object } export interface ConsentGroupConsented { - id: string - label: string - description: string - purposeOfCollection: string - showVerbDetails: boolean - verbs: XApiVerbConsented[] - isDefault: boolean + id: string + label: string + description: string + purposeOfCollection: string + showVerbDetails: boolean + verbs: XApiVerbConsented[] + isDefault: boolean } interface ConsentDeclaration { - id: string - description: string + id: string + description: string } export interface ProviderConsent { - id: string - name: string - superseded_by: number | null - createdAt: string - definition: ConsentDeclaration[] - supersedes?: ProviderConsent + id: string + name: string + superseded_by: number | null + createdAt: string + definition: ConsentDeclaration[] + supersedes?: ProviderConsent } export interface ProviderSchemaDefinition { - id: string - name: string - description: string - groups: ConsentGroupSchema[] - essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent + id: string + name: string + description: string + groups: ConsentGroupSchema[] + essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent } export interface ProviderSchema extends ConsentDeclaration { - superseded_by: number | null - createdAt: string - groups: ConsentGroupSchema[] - essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent - definition: ProviderSchemaDefinition + superseded_by: number | null + createdAt: string + groups: ConsentGroupSchema[] + essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent + definition: ProviderSchemaDefinition } export interface UserConsent extends ConsentDeclaration { - id: string - groups: ConsentGroupConsented[] - essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent + id: string + groups: ConsentGroupConsented[] + essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent + created?: Date } export interface UserConsentVerbs { - providerId: ProviderId - providerSchemaId: string - verbs: { id: string; consented?: boolean; objects: string }[] + providerId: ProviderId + providerSchemaId: string + verbs: { id: string; consented?: boolean; objects: string }[] } export type ProviderId = number @@ -103,20 +105,20 @@ export type ProviderId = number * @returns */ export const providerSchemaToUserConsent = (providerSchema: ProviderSchema): UserConsent => { - return { - id: providerSchema.id, - description: providerSchema.description, - groups: providerSchema.groups.map((group) => ({ - ...group, - verbs: group.verbs.map((verb) => ({ - ...verb, - consented: verb.defaultConsent, - objects: verb.objects ? verb.objects.map((object) => ({ - ...object, - consented: object.defaultConsent - })) : [] - })) - })), - essential_verbs: providerSchema.essential_verbs - } + return { + id: providerSchema.id, + description: providerSchema.description, + groups: providerSchema.groups.map((group) => ({ + ...group, + verbs: group.verbs.map((verb) => ({ + ...verb, + consented: verb.defaultConsent, + objects: verb.objects ? verb.objects.map((object) => ({ + ...object, + consented: object.defaultConsent + })) : [] + })) + })), + essential_verbs: providerSchema.essential_verbs + } } diff --git a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.html b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.html index 95ec032..08575b2 100644 --- a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.html +++ b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.html @@ -2,6 +2,7 @@ <nz-card *ngFor="let consentGroup of consentDeclaration.groups; index as idx" [nzTitle]="!consentGroup.isDefault ? consentGroup.label : ''" [nzExtra]="titleToggle"> <ng-template #titleToggle> <nz-switch + [nzDisabled]="preview" (click)="$event.stopPropagation()" (ngModelChange)="toggleGroup(consentGroup, $event)" [ngModel]="isGroupActive(consentGroup)"></nz-switch> @@ -32,7 +33,7 @@ [nzControl]="true" (click)="$event.stopPropagation();toggleVerb(verb.id)" [(ngModel)]="verb.consented" - [nzDisabled]="!consentGroup.isDefault"></nz-switch> + [nzDisabled]="preview || !consentGroup.isDefault"></nz-switch> <div>{{ verb.label }}</div> <fa-icon class="hint-icon" [icon]="faInfo"></fa-icon> <fa-icon @@ -54,7 +55,7 @@ [nzControl]="true" (click)="$event.stopPropagation();toggleObjectConsent(verb.id, object.id)" [ngModel]="object.consented" - [nzDisabled]="!consentGroup.isDefault"></nz-switch> + [nzDisabled]="preview || !consentGroup.isDefault"></nz-switch> <nz-switch nz-tooltip nzTooltipTitle="previous setting" diff --git a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts index 4173feb..7ff0b2f 100644 --- a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts +++ b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts @@ -31,6 +31,7 @@ export class PrivacySettingComponent implements OnInit { @Input() consentDeclaration!: UserConsent @Input() previousUserConsent: UserConsent | null = null @Input() deletable? = false + @Input() preview: boolean = false @Output() change: EventEmitter<UserConsent> = new EventEmitter<UserConsent>() diff --git a/src/frontend/src/app/navigation/header/header.component.html b/src/frontend/src/app/navigation/header/header.component.html index 10938c2..2711457 100644 --- a/src/frontend/src/app/navigation/header/header.component.html +++ b/src/frontend/src/app/navigation/header/header.component.html @@ -4,25 +4,34 @@ <ul nz-menu nzMode="horizontal"> <li nz-menu-item routerLink="/analytics-engines" - *ngIf="loggedIn && !loggedIn?.isProvider" + *ngIf="environment.pageVisibility.analyses && loggedIn && !loggedIn?.isProvider" i18n="Analyses | Header entry @@anaylticsEngineHeader" [nzMatchRouter]="true" > Analyses </li> + <li nz-menu-item + routerLink="/consent-history" + *ngIf="environment.pageVisibility.consent_history && loggedIn && !loggedIn?.isProvider" + i18n="Consent History | Header entry @@consentHistoryHeader" + [nzMatchRouter]="true" + > + Consent Management + </li> + <li nz-menu-item routerLink="/consent-management" - *ngIf="loggedIn && !loggedIn?.isProvider" + *ngIf="environment.pageVisibility.consent_management && loggedIn && !loggedIn?.isProvider" i18n="Consent Management | Header entry @@consentManagmentHeader" [nzMatchRouter]="true" > - Consent Management + Consent History </li> <li nz-menu-item routerLink="/merge-actors" - *ngIf="loggedIn && !loggedIn?.isProvider" + *ngIf="environment.pageVisibility.merge_data && loggedIn && !loggedIn?.isProvider" i18n="Merge Data | Header entry @@mergeDataHeader" [nzMatchRouter]="true" > diff --git a/src/frontend/src/app/navigation/header/header.component.ts b/src/frontend/src/app/navigation/header/header.component.ts index f630e7d..966be11 100644 --- a/src/frontend/src/app/navigation/header/header.component.ts +++ b/src/frontend/src/app/navigation/header/header.component.ts @@ -52,4 +52,6 @@ export class HeaderComponent implements OnInit { handleSSOClick(): void { window.location.href = `${environment.apiUrl}/login` } + + protected readonly environment = environment } diff --git a/src/frontend/src/app/services/api.service.ts b/src/frontend/src/app/services/api.service.ts index d800139..79b00b8 100644 --- a/src/frontend/src/app/services/api.service.ts +++ b/src/frontend/src/app/services/api.service.ts @@ -15,6 +15,17 @@ export interface UserConsentResponse { provider_schema?: ProviderSchema } +export interface GroupedConsentHistory { + created: Date, + consents: UserConsent[] +} + +export interface UserConsentHistoryResponse { + groups: GroupedConsentHistory[] + message?: boolean + no_consents_record?: boolean +} + export interface Provider { id: number name: string @@ -107,6 +118,12 @@ export class ApiService { ) } + getUserConsentHistory(): Observable<UserConsentHistoryResponse> { + return this.http.get<UserConsentHistoryResponse>( + `${environment.apiUrl}/api/v1/consents/user/history` + ) + } + saveUserConsent(data: UserConsentVerbs[]): Observable<UserConsentResponse> { return this.http.post<UserConsentResponse>( `${environment.apiUrl}/api/v1/consents/user/save`, diff --git a/src/frontend/src/environments/environment.prod.ts b/src/frontend/src/environments/environment.prod.ts index e76a9fe..f169dab 100644 --- a/src/frontend/src/environments/environment.prod.ts +++ b/src/frontend/src/environments/environment.prod.ts @@ -1,4 +1,10 @@ export const environment = { production: true, - apiUrl: '' + apiUrl: '', + pageVisibility: { + analyses: true, + consent_history: true, + consent_management: true, + merge_data: true + } }; diff --git a/src/frontend/src/environments/environment.ts b/src/frontend/src/environments/environment.ts index 3e1f27d..52d12b5 100644 --- a/src/frontend/src/environments/environment.ts +++ b/src/frontend/src/environments/environment.ts @@ -4,7 +4,13 @@ export const environment = { production: false, - apiUrl: 'http://127.0.0.1:8000' + apiUrl: 'http://127.0.0.1:8000', + pageVisibility: { + analyses: true, + consent_history: true, + consent_management: true, + merge_data: true + } }; /* diff --git a/src/frontend/src/favicon.ico b/src/frontend/src/favicon.ico deleted file mode 100644 index 997406ad22c29aae95893fb3d666c30258a09537..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 948 zcmeAS@N?(olHy`uVBq!ia0y~yV31*8V36ZrV_;ygKUz1Rfq{Xuz$3Dlfq`2Hgc&d0 zt^32kz?|mk;uzx5IW^qdM>tSq@8_3ird$#$b)PtmGj%avUXLD+Sz&S#OH+=Bed~^k zE(t5$l#lUxmp*pmH4IGd;gaZCq}bXq{|D!ekIpR10+!S`*{d|ox^icu@gBap=gxSl z)qcKL`~J+{;`cM7wI-iz2<>oODaSCydtP5<MW1wf<TuBx!dGjq8vAe_U9d%#;n2;~ ztIzqnEH{y!wal35e7{x1Hsg%~d-G=p@OUvc9hsl6?CH^zIAcH4dH&5h)pmyZ`;AmY znu9nUgJNH3mcPBjX#coaKr*U4j!%Z~oA~*{^O0@2@w29<wr*Hd6m9W4?iJTNwSxEO zbZ7LQcdSjGd{tNB!=E*W4}Mcz_;n3qgHgODkLx_m$6c$kW=vUJwv9RWv=qawDF4jI zecb79^1?aT*Gg5}<XPMZSbg{U^>^))gz^vCs#x@S-}tn_vPR|kz1LqIe!rYt))!zS zvoY(Akln64y_z>0QjMl1s!cRVJi24!LcTlil2T>5&o<0i>s<dc?er26S5@8W8`pM; z?OJ_(o%8XhesAw(MP$yZOFrqWQ!;;jh~)Cir!V|3V%SyvTVbY;X7SZaVH{WAn5iz& z=um#RWTxWF*#F&(9!oDx@K6yA5`DY&SzM6FU&o1Nh8mJ$_K`0-W?itG(RWrOO6tS( z!$-ac=AQN8Fk0<BB}&XQ`TaF6*~v@SoLSx?;oQ+;r1+?{g<-$Og0AfnQ<NAsxaYUD z+I=d#@j<}$!(6siDyny?5-zS^_E<WE|Fc@(z6)mqJUBACq7-f4Ff?>mExzc#e(SB@ zpT5qXq@xuU7Pi&!lysUFld}wm*K3YgSJ&p#-Wvt7zg$RJsLW@&^@+eF`2?fN5XBd} zYR&gAzj?o-u2HA8y}zpX#FqnS-c)`HnSE9~tZm`$gD*b2i2bTRB<E}-y2QP;#X<GV zM02Ca4I=mURp)zkC^<~)h~voUs^i%;FYU;h>57S0T%Iz2GuuD?dAA3DV$B=lZOvyH z6r$};vfMbhvpd#2)ab(g!yh-Ek7Z~$GM#H>zhpq@2A(^5;i*}LzgRN!Zg8aEKBmo3 zU~_+0pvT;%56#ba?9GZ^(LVK@^z27^d%wKo4BR!RsX!tAv-hD-_1uOUt()YuJC_P2 zIa#nij44Xwx_+jyrA#kp&fiDVG#=mia(wT_^268u`LO+C{8`>(vCFyVAp-*ggQu&X J%Q~loCIA*?vmyWh diff --git a/src/frontend/src/locale/messages.de.xlf b/src/frontend/src/locale/messages.de.xlf index 943538b..6a602eb 100644 --- a/src/frontend/src/locale/messages.de.xlf +++ b/src/frontend/src/locale/messages.de.xlf @@ -445,7 +445,7 @@ <context context-type="linenumber">243</context> </context-group> <context-group purpose="location"> - <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.ts</context> + <context context-type="sourcefile">src/app/consent-management/wizard/consent-history.component.ts</context> <context context-type="linenumber">159</context> </context-group> </trans-unit> @@ -1793,6 +1793,15 @@ <note priority="1" from="description"> Header entry </note> <note priority="1" from="meaning">Consent Management </note> </trans-unit> + <trans-unit id="consentHistoryHeader" datatype="html"> + <source>Consent History</source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> + <context context-type="linenumber">21</context> + </context-group> + <note priority="1" from="description"> Header entry </note> + <note priority="1" from="meaning">Consent History </note> + </trans-unit> <trans-unit id="applicationTokens" datatype="html"> <source>Application Tokens</source> <context-group purpose="location"> diff --git a/src/frontend/src/locale/messages.xlf b/src/frontend/src/locale/messages.xlf index c793fe8..d9fe431 100644 --- a/src/frontend/src/locale/messages.xlf +++ b/src/frontend/src/locale/messages.xlf @@ -437,7 +437,7 @@ <context context-type="linenumber">243</context> </context-group> <context-group purpose="location"> - <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.ts</context> + <context context-type="sourcefile">src/app/consent-management/wizard/consent-history.component.ts</context> <context context-type="linenumber">159</context> </context-group> </trans-unit> @@ -1119,6 +1119,15 @@ <note priority="1" from="description"> Header entry </note> <note priority="1" from="meaning">Consent Management </note> </trans-unit> + <trans-unit id="consentHistoryHeader" datatype="html"> + <source>Consent History</source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> + <context context-type="linenumber">21</context> + </context-group> + <note priority="1" from="description"> Header entry </note> + <note priority="1" from="meaning">Consent History </note> + </trans-unit> <trans-unit id="mergeDataHeader" datatype="html"> <source>Merge Data</source> <context-group purpose="location"> -- GitLab