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