feat(security & admin): update firestore and storage rules with admin token check and enable admin deletion in app
This commit is contained in:
+32
-25
@@ -1,35 +1,42 @@
|
|||||||
rules_version = '2';
|
rules_version = '2';
|
||||||
service cloud.firestore {
|
service cloud.firestore {
|
||||||
match /databases/{database}/documents {
|
match /databases/{database}/documents {
|
||||||
|
|
||||||
// News / Neuigkeiten: Öffentlich lesbar, Schreiben für angemeldete Nutzer
|
// Hilfsfunktionen
|
||||||
match /News/{document=**} {
|
function isAuthenticated() {
|
||||||
allow read: if true;
|
return request.auth != null;
|
||||||
allow write: if request.auth != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mängel & Anliegen: Öffentlich lesbar, Erstellen angemeldet, Löschen nur eigener Autor
|
function isAdmin() {
|
||||||
match /Issues/{document} {
|
return request.auth != null && request.auth.token.admin == true;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bürgerforum / Ideen: Öffentlich lesbar, Erstellen angemeldet, Löschen eigener Autor
|
function isAuthorOrAdmin() {
|
||||||
match /CitizensForum/{document} {
|
return isAuthenticated() && (
|
||||||
allow read: if true;
|
(resource.data.author_uid != null && resource.data.author_uid == request.auth.uid) ||
|
||||||
allow create: if request.auth != null;
|
(resource.data.authorUid != null && resource.data.authorUid == request.auth.uid) ||
|
||||||
allow update: if request.auth != null;
|
isAdmin()
|
||||||
allow delete: if request.auth != null &&
|
);
|
||||||
(resource.data.author_uid == request.auth.uid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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} {
|
match /ContactMessages/{document} {
|
||||||
allow create: if request.auth != null && request.auth.token.firebase.sign_in_provider != 'anonymous';
|
// Senden nur für registrierte Nutzer (nicht-anonym)
|
||||||
allow read, update, delete: if request.auth != null;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -111,7 +110,8 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currentUser = FirebaseAuth.instance.currentUser;
|
final auth = Provider.of<AuthService>(context);
|
||||||
|
final currentUser = auth.user;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -240,6 +240,7 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
final isSelected = _selectedItems.containsKey(item.id);
|
final isSelected = _selectedItems.containsKey(item.id);
|
||||||
final isAuthor =
|
final isAuthor =
|
||||||
currentUser != null && item.authorUid == currentUser.uid;
|
currentUser != null && item.authorUid == currentUser.uid;
|
||||||
|
final canDelete = isAuthor || auth.isAdmin;
|
||||||
final formattedDate =
|
final formattedDate =
|
||||||
DateFormat('dd.MM.yyyy').format(item.createdDate);
|
DateFormat('dd.MM.yyyy').format(item.createdDate);
|
||||||
|
|
||||||
@@ -367,7 +368,7 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAuthor) {
|
if (canDelete) {
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
key: Key(item.id),
|
key: Key(item.id),
|
||||||
direction: DismissDirection.endToStart,
|
direction: DismissDirection.endToStart,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class AuthService extends ChangeNotifier {
|
|||||||
|
|
||||||
String? _cachedFirstName;
|
String? _cachedFirstName;
|
||||||
String? _cachedLastName;
|
String? _cachedLastName;
|
||||||
|
bool _isAdmin = false;
|
||||||
|
bool get isAdmin => _isAdmin;
|
||||||
|
|
||||||
String get firstName {
|
String get firstName {
|
||||||
if (_cachedFirstName != null && _cachedFirstName!.isNotEmpty) {
|
if (_cachedFirstName != null && _cachedFirstName!.isNotEmpty) {
|
||||||
@@ -45,14 +47,28 @@ class AuthService extends ChangeNotifier {
|
|||||||
_initAuth();
|
_initAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _initAuth() async {
|
Future<void> _initAuth() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
_isCommunityUnlocked = prefs.getBool(_prefKeyUnlocked) ?? false;
|
_isCommunityUnlocked = prefs.getBool(_prefKeyUnlocked) ?? false;
|
||||||
_cachedFirstName = prefs.getString('user_first_name');
|
_cachedFirstName = prefs.getString('user_first_name');
|
||||||
_cachedLastName = prefs.getString('user_last_name');
|
_cachedLastName = prefs.getString('user_last_name');
|
||||||
|
|
||||||
_auth.authStateChanges().listen((User? user) {
|
_auth.authStateChanges().listen((User? user) async {
|
||||||
_user = user;
|
_user = user;
|
||||||
|
await _checkAdminClaim(user);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,10 +168,11 @@ class AuthService extends ChangeNotifier {
|
|||||||
/// Check if user's email is verified
|
/// Check if user's email is verified
|
||||||
bool get isEmailVerified => _user?.emailVerified ?? false;
|
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<void> reloadUser() async {
|
Future<void> reloadUser() async {
|
||||||
await _user?.reload();
|
await _user?.reload();
|
||||||
_user = _auth.currentUser;
|
_user = _auth.currentUser;
|
||||||
|
await _checkAdminClaim(_user);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +226,7 @@ class AuthService extends ChangeNotifier {
|
|||||||
await prefs.remove('user_last_name');
|
await prefs.remove('user_last_name');
|
||||||
_cachedFirstName = null;
|
_cachedFirstName = null;
|
||||||
_cachedLastName = null;
|
_cachedLastName = null;
|
||||||
|
_isAdmin = false;
|
||||||
await _auth.signOut();
|
await _auth.signOut();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
|||||||
+5
-2
@@ -12,8 +12,11 @@ service firebase.storage {
|
|||||||
&& request.resource.size < 10 * 1024 * 1024
|
&& request.resource.size < 10 * 1024 * 1024
|
||||||
&& request.resource.contentType.matches('image/.*');
|
&& request.resource.contentType.matches('image/.*');
|
||||||
|
|
||||||
// Nur der Verfasser (oder berechtigte Nutzer) darf sein Foto löschen
|
// Nur der Verfasser oder ein Admin darf sein Foto löschen
|
||||||
allow delete: if request.auth != null && request.auth.uid == userId;
|
allow delete: if request.auth != null && (
|
||||||
|
request.auth.uid == userId ||
|
||||||
|
request.auth.token.admin == true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user