diff --git a/firestore.rules b/firestore.rules index ecd9f48..594ad7f 100644 --- a/firestore.rules +++ b/firestore.rules @@ -1,35 +1,42 @@ rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { - - // News / Neuigkeiten: Öffentlich lesbar, Schreiben für angemeldete Nutzer - match /News/{document=**} { - allow read: if true; - allow write: if request.auth != null; + + // Hilfsfunktionen + function isAuthenticated() { + return request.auth != null; } - - // Mängel & Anliegen: Öffentlich lesbar, Erstellen angemeldet, Löschen nur eigener Autor - match /Issues/{document} { - allow read: if true; - allow create: if request.auth != null; - allow update: if request.auth != null; - allow delete: if request.auth != null && - (resource.data.author_uid == request.auth.uid); + + function isAdmin() { + return request.auth != null && request.auth.token.admin == true; } - - // Bürgerforum / Ideen: Öffentlich lesbar, Erstellen angemeldet, Löschen eigener Autor - match /CitizensForum/{document} { - allow read: if true; - allow create: if request.auth != null; - allow update: if request.auth != null; - allow delete: if request.auth != null && - (resource.data.author_uid == request.auth.uid); + + function isAuthorOrAdmin() { + return isAuthenticated() && ( + (resource.data.author_uid != null && resource.data.author_uid == request.auth.uid) || + (resource.data.authorUid != null && resource.data.authorUid == request.auth.uid) || + isAdmin() + ); } - - // Kontakt-Nachrichten: Nur registrierte Nutzer können einsenden, Lesen für berechtigte Nutzer + + // Öffentliche Sammlungen: News, Mängel (Issues) & Ideen (CitizensForum) + match /{collection}/{document=**} { + // Jeder darf News, Mängel & Ideen öffentlich lesen + allow read: if collection in ['News', 'Issues', 'CitizensForum']; + + // Erstellen darf jeder freigeschaltete/angemeldete Nutzer + allow create: if isAuthenticated() && collection in ['News', 'Issues', 'CitizensForum']; + + // Ändern/Löschen nur der eigene Autor oder Admin + allow update, delete: if isAuthorOrAdmin() && collection in ['News', 'Issues', 'CitizensForum']; + } + + // Kontakt-Nachrichten: Geschützt (nur registrierte Nutzer senden, Lesen nur Admin & Autor) match /ContactMessages/{document} { - allow create: if request.auth != null && request.auth.token.firebase.sign_in_provider != 'anonymous'; - allow read, update, delete: if request.auth != null; + // Senden nur für registrierte Nutzer (nicht-anonym) + allow create: if isAuthenticated() && request.auth.token.firebase.sign_in_provider != 'anonymous'; + // Lesen, Ändern und Löschen für Admin und Autor + allow read, update, delete: if isAuthorOrAdmin(); } } } diff --git a/lib/screens/post_list_screen.dart b/lib/screens/post_list_screen.dart index 1b899d4..ffbd643 100644 --- a/lib/screens/post_list_screen.dart +++ b/lib/screens/post_list_screen.dart @@ -1,5 +1,4 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -111,7 +110,8 @@ class _PostListScreenState extends State { @override Widget build(BuildContext context) { - final currentUser = FirebaseAuth.instance.currentUser; + final auth = Provider.of(context); + final currentUser = auth.user; return Scaffold( appBar: AppBar( @@ -240,6 +240,7 @@ class _PostListScreenState extends State { final isSelected = _selectedItems.containsKey(item.id); final isAuthor = currentUser != null && item.authorUid == currentUser.uid; + final canDelete = isAuthor || auth.isAdmin; final formattedDate = DateFormat('dd.MM.yyyy').format(item.createdDate); @@ -367,7 +368,7 @@ class _PostListScreenState extends State { ), ); - if (isAuthor) { + if (canDelete) { return Dismissible( key: Key(item.id), direction: DismissDirection.endToStart, diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index bc0c601..b1a717a 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -20,6 +20,8 @@ class AuthService extends ChangeNotifier { String? _cachedFirstName; String? _cachedLastName; + bool _isAdmin = false; + bool get isAdmin => _isAdmin; String get firstName { if (_cachedFirstName != null && _cachedFirstName!.isNotEmpty) { @@ -45,14 +47,28 @@ class AuthService extends ChangeNotifier { _initAuth(); } + Future _checkAdminClaim(User? user) async { + if (user != null && !user.isAnonymous) { + try { + final tokenResult = await user.getIdTokenResult(); + _isAdmin = tokenResult.claims?['admin'] == true; + } catch (_) { + _isAdmin = false; + } + } else { + _isAdmin = false; + } + } + Future _initAuth() async { final prefs = await SharedPreferences.getInstance(); _isCommunityUnlocked = prefs.getBool(_prefKeyUnlocked) ?? false; _cachedFirstName = prefs.getString('user_first_name'); _cachedLastName = prefs.getString('user_last_name'); - _auth.authStateChanges().listen((User? user) { + _auth.authStateChanges().listen((User? user) async { _user = user; + await _checkAdminClaim(user); notifyListeners(); }); @@ -152,10 +168,11 @@ class AuthService extends ChangeNotifier { /// Check if user's email is verified bool get isEmailVerified => _user?.emailVerified ?? false; - /// Reload current user from Firebase to refresh emailVerified status + /// Reload current user from Firebase to refresh emailVerified status and admin claim Future reloadUser() async { await _user?.reload(); _user = _auth.currentUser; + await _checkAdminClaim(_user); notifyListeners(); } @@ -209,6 +226,7 @@ class AuthService extends ChangeNotifier { await prefs.remove('user_last_name'); _cachedFirstName = null; _cachedLastName = null; + _isAdmin = false; await _auth.signOut(); } catch (e) { if (kDebugMode) { diff --git a/storage.rules b/storage.rules index 3a7f476..df16fb1 100644 --- a/storage.rules +++ b/storage.rules @@ -12,8 +12,11 @@ service firebase.storage { && request.resource.size < 10 * 1024 * 1024 && request.resource.contentType.matches('image/.*'); - // Nur der Verfasser (oder berechtigte Nutzer) darf sein Foto löschen - allow delete: if request.auth != null && request.auth.uid == userId; + // Nur der Verfasser oder ein Admin darf sein Foto löschen + allow delete: if request.auth != null && ( + request.auth.uid == userId || + request.auth.token.admin == true + ); } } }