This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-sdk/src/mobile/flutter/consent_sdk.dart
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

659 lines
19 KiB
Dart

/// Flutter Consent SDK
///
/// DSGVO/TTDSG-konformes Consent Management fuer Flutter Apps.
///
/// Nutzung:
/// 1. In main() mit ConsentManager.configure() initialisieren
/// 2. App mit ConsentProvider wrappen
/// 3. Mit ConsentGate Inhalte schuetzen
///
/// Copyright (c) 2025 BreakPilot
/// Apache License 2.0
library consent_sdk;
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
// =============================================================================
// Consent Categories
// =============================================================================
/// Standard-Consent-Kategorien nach IAB TCF 2.2
enum ConsentCategory {
essential, // Technisch notwendig
functional, // Personalisierung
analytics, // Nutzungsanalyse
marketing, // Werbung
social, // Social Media
}
// =============================================================================
// Consent State
// =============================================================================
/// Aktueller Consent-Zustand
class ConsentState {
final Map<ConsentCategory, bool> categories;
final Map<String, bool> vendors;
final DateTime timestamp;
final String version;
final String? consentId;
final DateTime? expiresAt;
final String? tcfString;
const ConsentState({
required this.categories,
this.vendors = const {},
required this.timestamp,
this.version = '1.0.0',
this.consentId,
this.expiresAt,
this.tcfString,
});
/// Default State mit nur essential = true
factory ConsentState.defaultState() {
return ConsentState(
categories: {
ConsentCategory.essential: true,
ConsentCategory.functional: false,
ConsentCategory.analytics: false,
ConsentCategory.marketing: false,
ConsentCategory.social: false,
},
timestamp: DateTime.now(),
);
}
ConsentState copyWith({
Map<ConsentCategory, bool>? categories,
Map<String, bool>? vendors,
DateTime? timestamp,
String? version,
String? consentId,
DateTime? expiresAt,
String? tcfString,
}) {
return ConsentState(
categories: categories ?? this.categories,
vendors: vendors ?? this.vendors,
timestamp: timestamp ?? this.timestamp,
version: version ?? this.version,
consentId: consentId ?? this.consentId,
expiresAt: expiresAt ?? this.expiresAt,
tcfString: tcfString ?? this.tcfString,
);
}
Map<String, dynamic> toJson() => {
'categories': categories.map((k, v) => MapEntry(k.name, v)),
'vendors': vendors,
'timestamp': timestamp.toIso8601String(),
'version': version,
'consentId': consentId,
'expiresAt': expiresAt?.toIso8601String(),
'tcfString': tcfString,
};
factory ConsentState.fromJson(Map<String, dynamic> json) {
return ConsentState(
categories: (json['categories'] as Map<String, dynamic>).map(
(k, v) => MapEntry(
ConsentCategory.values.firstWhere((e) => e.name == k),
v as bool,
),
),
vendors: Map<String, bool>.from(json['vendors'] ?? {}),
timestamp: DateTime.parse(json['timestamp']),
version: json['version'] ?? '1.0.0',
consentId: json['consentId'],
expiresAt: json['expiresAt'] != null
? DateTime.parse(json['expiresAt'])
: null,
tcfString: json['tcfString'],
);
}
}
// =============================================================================
// Configuration
// =============================================================================
/// SDK-Konfiguration
class ConsentConfig {
final String apiEndpoint;
final String siteId;
final String language;
final bool showRejectAll;
final bool showAcceptAll;
final bool granularControl;
final int rememberDays;
final bool debug;
const ConsentConfig({
required this.apiEndpoint,
required this.siteId,
this.language = 'en',
this.showRejectAll = true,
this.showAcceptAll = true,
this.granularControl = true,
this.rememberDays = 365,
this.debug = false,
});
}
// =============================================================================
// Consent Manager
// =============================================================================
/// Haupt-Manager fuer Consent-Verwaltung
class ConsentManager extends ChangeNotifier {
// Singleton
static ConsentManager? _instance;
static ConsentManager get instance => _instance!;
// State
ConsentState _consent = ConsentState.defaultState();
bool _isInitialized = false;
bool _isLoading = true;
bool _isBannerVisible = false;
bool _isSettingsVisible = false;
// Private
ConsentConfig? _config;
late ConsentStorage _storage;
late ConsentApiClient _apiClient;
// Getters
ConsentState get consent => _consent;
bool get isInitialized => _isInitialized;
bool get isLoading => _isLoading;
bool get isBannerVisible => _isBannerVisible;
bool get isSettingsVisible => _isSettingsVisible;
bool get needsConsent => _consent.consentId == null;
// Private constructor
ConsentManager._();
/// Konfiguriert den ConsentManager
/// Sollte in main() vor runApp() aufgerufen werden
static Future<void> configure(ConsentConfig config) async {
_instance = ConsentManager._();
_instance!._config = config;
_instance!._storage = ConsentStorage();
_instance!._apiClient = ConsentApiClient(
baseUrl: config.apiEndpoint,
siteId: config.siteId,
);
if (config.debug) {
debugPrint('[ConsentSDK] Configured with siteId: ${config.siteId}');
}
await _instance!._initialize();
}
// ==========================================================================
// Initialization
// ==========================================================================
Future<void> _initialize() async {
try {
// Lokalen Consent laden
final stored = await _storage.load();
if (stored != null) {
// Pruefen ob abgelaufen
if (stored.expiresAt != null &&
DateTime.now().isAfter(stored.expiresAt!)) {
_consent = ConsentState.defaultState();
await _storage.clear();
} else {
_consent = stored;
}
}
// Vom Server synchronisieren
try {
final fingerprint = await DeviceFingerprint.generate();
final serverConsent = await _apiClient.getConsent(fingerprint);
if (serverConsent != null) {
_consent = serverConsent;
await _storage.save(_consent);
}
} catch (e) {
if (_config?.debug == true) {
debugPrint('[ConsentSDK] Failed to sync consent: $e');
}
}
_isInitialized = true;
// Banner anzeigen falls noetig
if (needsConsent) {
showBanner();
}
} finally {
_isLoading = false;
notifyListeners();
}
}
// ==========================================================================
// Public API
// ==========================================================================
/// Prueft ob Consent fuer Kategorie erteilt wurde
bool hasConsent(ConsentCategory category) {
// Essential ist immer erlaubt
if (category == ConsentCategory.essential) return true;
return _consent.categories[category] ?? false;
}
/// Alle Kategorien akzeptieren
Future<void> acceptAll() async {
final newCategories = {
for (var cat in ConsentCategory.values) cat: true
};
final newConsent = _consent.copyWith(
categories: newCategories,
timestamp: DateTime.now(),
);
await _saveConsent(newConsent);
hideBanner();
}
/// Alle nicht-essentiellen Kategorien ablehnen
Future<void> rejectAll() async {
final newCategories = {
for (var cat in ConsentCategory.values)
cat: cat == ConsentCategory.essential
};
final newConsent = _consent.copyWith(
categories: newCategories,
timestamp: DateTime.now(),
);
await _saveConsent(newConsent);
hideBanner();
}
/// Auswahl speichern
Future<void> saveSelection(Map<ConsentCategory, bool> categories) async {
final updated = Map<ConsentCategory, bool>.from(categories);
updated[ConsentCategory.essential] = true; // Essential immer true
final newConsent = _consent.copyWith(
categories: updated,
timestamp: DateTime.now(),
);
await _saveConsent(newConsent);
hideBanner();
}
// ==========================================================================
// UI Control
// ==========================================================================
void showBanner() {
_isBannerVisible = true;
notifyListeners();
}
void hideBanner() {
_isBannerVisible = false;
notifyListeners();
}
void showSettings() {
_isSettingsVisible = true;
notifyListeners();
}
void hideSettings() {
_isSettingsVisible = false;
notifyListeners();
}
// ==========================================================================
// Private Methods
// ==========================================================================
Future<void> _saveConsent(ConsentState newConsent) async {
// Lokal speichern
await _storage.save(newConsent);
// An Server senden
try {
final fingerprint = await DeviceFingerprint.generate();
final response = await _apiClient.saveConsent(newConsent, fingerprint);
final updated = newConsent.copyWith(
consentId: response['consentId'],
expiresAt: DateTime.parse(response['expiresAt']),
);
_consent = updated;
await _storage.save(updated);
} catch (e) {
// Lokal speichern auch bei Fehler
_consent = newConsent;
if (_config?.debug == true) {
debugPrint('[ConsentSDK] Failed to sync consent: $e');
}
}
notifyListeners();
}
}
// =============================================================================
// Storage
// =============================================================================
/// Sichere Speicherung mit flutter_secure_storage
class ConsentStorage {
final _storage = const FlutterSecureStorage();
static const _key = 'breakpilot_consent_state';
Future<ConsentState?> load() async {
final data = await _storage.read(key: _key);
if (data == null) return null;
try {
return ConsentState.fromJson(jsonDecode(data));
} catch (e) {
return null;
}
}
Future<void> save(ConsentState consent) async {
final data = jsonEncode(consent.toJson());
await _storage.write(key: _key, value: data);
}
Future<void> clear() async {
await _storage.delete(key: _key);
}
}
// =============================================================================
// API Client
// =============================================================================
/// API Client fuer Backend-Kommunikation
class ConsentApiClient {
final String baseUrl;
final String siteId;
ConsentApiClient({
required this.baseUrl,
required this.siteId,
});
Future<ConsentState?> getConsent(String fingerprint) async {
final response = await http.get(
Uri.parse('$baseUrl/banner/consent?site_id=$siteId&fingerprint=$fingerprint'),
);
if (response.statusCode != 200) return null;
return ConsentState.fromJson(jsonDecode(response.body));
}
Future<Map<String, dynamic>> saveConsent(
ConsentState consent,
String fingerprint,
) async {
final response = await http.post(
Uri.parse('$baseUrl/banner/consent'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'site_id': siteId,
'device_fingerprint': fingerprint,
'categories': consent.categories.map((k, v) => MapEntry(k.name, v)),
'vendors': consent.vendors,
'platform': Platform.isIOS ? 'ios' : 'android',
'app_version': '1.0.0', // TODO: Get from package_info_plus
}),
);
return jsonDecode(response.body);
}
}
// =============================================================================
// Device Fingerprint
// =============================================================================
/// Privacy-konformer Device Fingerprint
class DeviceFingerprint {
static Future<String> generate() async {
final deviceInfo = DeviceInfoPlugin();
String rawId;
if (Platform.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
rawId = '${iosInfo.identifierForVendor}-${iosInfo.model}-${iosInfo.systemVersion}';
} else if (Platform.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
rawId = '${androidInfo.id}-${androidInfo.model}-${androidInfo.version.sdkInt}';
} else {
rawId = DateTime.now().millisecondsSinceEpoch.toString();
}
// SHA-256 Hash
final bytes = utf8.encode(rawId);
final digest = sha256.convert(bytes);
return digest.toString();
}
}
// =============================================================================
// Flutter Widgets
// =============================================================================
/// Consent Provider - Wraps the app
class ConsentProvider extends StatelessWidget {
final Widget child;
const ConsentProvider({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: ConsentManager.instance,
child: child,
);
}
}
/// Consent Gate - Zeigt Inhalt nur bei Consent
class ConsentGate extends StatelessWidget {
final ConsentCategory category;
final Widget child;
final Widget? placeholder;
final Widget? loading;
const ConsentGate({
super.key,
required this.category,
required this.child,
this.placeholder,
this.loading,
});
@override
Widget build(BuildContext context) {
return Consumer<ConsentManager>(
builder: (context, consent, _) {
if (consent.isLoading) {
return loading ?? const SizedBox.shrink();
}
if (!consent.hasConsent(category)) {
return placeholder ?? const SizedBox.shrink();
}
return child;
},
);
}
}
/// Consent Banner - Default Banner UI
class ConsentBanner extends StatelessWidget {
final String? title;
final String? description;
final String? acceptAllText;
final String? rejectAllText;
final String? settingsText;
const ConsentBanner({
super.key,
this.title,
this.description,
this.acceptAllText,
this.rejectAllText,
this.settingsText,
});
@override
Widget build(BuildContext context) {
return Consumer<ConsentManager>(
builder: (context, consent, _) {
if (!consent.isBannerVisible) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, -5),
),
],
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title ?? 'Datenschutzeinstellungen',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
Text(
description ??
'Wir nutzen Cookies und ähnliche Technologien, um Ihnen ein optimales Nutzererlebnis zu bieten.',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: consent.rejectAll,
child: Text(rejectAllText ?? 'Alle ablehnen'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: consent.showSettings,
child: Text(settingsText ?? 'Einstellungen'),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton(
onPressed: consent.acceptAll,
child: Text(acceptAllText ?? 'Alle akzeptieren'),
),
),
],
),
],
),
),
);
},
);
}
}
/// Consent Placeholder - Placeholder fuer blockierten Inhalt
class ConsentPlaceholder extends StatelessWidget {
final ConsentCategory category;
final String? message;
final String? buttonText;
const ConsentPlaceholder({
super.key,
required this.category,
this.message,
this.buttonText,
});
String get _categoryName {
switch (category) {
case ConsentCategory.essential:
return 'Essentielle Cookies';
case ConsentCategory.functional:
return 'Funktionale Cookies';
case ConsentCategory.analytics:
return 'Statistik-Cookies';
case ConsentCategory.marketing:
return 'Marketing-Cookies';
case ConsentCategory.social:
return 'Social Media-Cookies';
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
message ?? 'Dieser Inhalt erfordert $_categoryName.',
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: ConsentManager.instance.showSettings,
child: Text(buttonText ?? 'Cookie-Einstellungen öffnen'),
),
],
),
);
}
}
// =============================================================================
// Extension for easy context access
// =============================================================================
extension ConsentExtension on BuildContext {
ConsentManager get consent => Provider.of<ConsentManager>(this, listen: false);
bool hasConsent(ConsentCategory category) => consent.hasConsent(category);
}