From 174ee5f9f152d03c26841ae51fe1960aefa33f51 Mon Sep 17 00:00:00 2001 From: Moataz Farid Date: Wed, 8 Jan 2025 08:32:44 +0200 Subject: [PATCH] Refactor Firestore Rules and Enhance Data Synchronization Components - Updated Firestore security rules to include new validation functions for feeding rounds, treasury categories, and transactions, ensuring data integrity and proper access control. - Introduced a new DataSynchronizer component to synchronize data between React Query and Zustand store, improving state management across the application. - Added TreasuryValidation component to validate completed payments against treasury categories, enhancing financial oversight and error detection. - Enhanced FeedingRoundModal with improved category handling and UI updates, including better error messaging and category selection. - Updated FeedingRoundList to support new status transitions and improved user feedback for round cancellations. - Refactored treasury service methods to decouple feeding round creation from treasury adjustments, ensuring clearer transaction handling. --- firebase.rules | 69 +-- src/components/DataSynchronizer.tsx | 34 ++ src/components/Layout.tsx | 4 + src/components/TreasuryValidation.tsx | 80 ++++ src/components/modals/FeedingRoundModal.tsx | 470 ++++++++++++------- src/pages/FeedingRoundList.tsx | 161 ++++++- src/pages/TreasuryList.tsx | 20 +- src/services/firebase/feedingRoundService.ts | 290 ++++++++---- src/services/firebase/treasuryService.ts | 1 + src/utils/treasuryValidator.ts | 59 +++ 10 files changed, 858 insertions(+), 330 deletions(-) create mode 100644 src/components/DataSynchronizer.tsx create mode 100644 src/components/TreasuryValidation.tsx create mode 100644 src/utils/treasuryValidator.ts diff --git a/firebase.rules b/firebase.rules index b4fe0cf..d3194a9 100644 --- a/firebase.rules +++ b/firebase.rules @@ -6,60 +6,71 @@ service cloud.firestore { return request.auth != null; } - function isAdmin() { - return isSignedIn() && request.auth.token.admin == true; - } - - function isValidAmount() { - return request.resource.data.amount is number && request.resource.data.amount > 0; - } - function hasValidTimestamps() { let hasCreatedAt = request.resource.data.createdAt is timestamp; let hasUpdatedAt = request.resource.data.updatedAt is timestamp; return hasCreatedAt && hasUpdatedAt; } - // Common rules - match /{document=**} { - allow read: if isSignedIn(); - allow write: if isAdmin(); - } - - // Specific collection rules - match /donors/{donorId} { - allow read: if isSignedIn(); - allow write: if isAdmin(); + function isValidFeedingRound() { + let data = request.resource.data; + return data.allocatedAmount is number + && data.allocatedAmount > 0 + && data.unitPrice is number + && data.unitPrice > 0 + && data.categoryId is string + && data.date is string + && (data.status == 'PENDING' || data.status == 'IN_PROGRESS' || data.status == 'COMPLETED' || data.status == 'CANCELLED') + && (!data.keys().hasAny(['photos']) || data.photos is list); } - match /donations/{donationId} { - allow read: if isSignedIn(); - allow write: if isAdmin() && isValidAmount() && hasValidTimestamps(); + function isValidTreasuryCategory() { + let data = request.resource.data; + return data.name is string + && data.name.size() > 0 + && data.balance is number + && data.balance >= 0; } - match /beneficiaries/{beneficiaryId} { - allow read: if isSignedIn(); - allow write: if isAdmin() && hasValidTimestamps(); + function isValidTransaction() { + let data = request.resource.data; + return data.type in ['DEBIT', 'CREDIT', 'STATUS_UPDATE'] + && data.amount is number + && data.amount > 0 + && data.description is string + && data.category is string + && data.reference is string + && data.status in ['PENDING', 'COMPLETED', 'CANCELLED']; } - match /payments/{paymentId} { + // Common rules - all operations require authentication + match /{document=**} { allow read: if isSignedIn(); - allow write: if isAdmin() && isValidAmount() && hasValidTimestamps(); + allow write: if isSignedIn(); } + // Feeding rounds collection match /feedingRounds/{roundId} { allow read: if isSignedIn(); - allow write: if isAdmin() && isValidAmount() && hasValidTimestamps(); + allow create: if isSignedIn() && isValidFeedingRound() && hasValidTimestamps(); + allow update: if isSignedIn() && isValidFeedingRound() && hasValidTimestamps(); + allow delete: if isSignedIn(); } + // Treasury collection match /treasury/{categoryId} { allow read: if isSignedIn(); - allow write: if isAdmin() && isValidAmount() && hasValidTimestamps(); + allow create: if isSignedIn() && isValidTreasuryCategory() && hasValidTimestamps(); + allow update: if isSignedIn() && isValidTreasuryCategory() && hasValidTimestamps(); + allow delete: if isSignedIn(); } + // Transactions collection match /transactions/{transactionId} { allow read: if isSignedIn(); - allow write: if isAdmin() && isValidAmount() && hasValidTimestamps(); + allow create: if isSignedIn() && isValidTransaction() && hasValidTimestamps(); + allow update: if false; // Transactions should never be updated + allow delete: if false; // Transactions should never be deleted } } } \ No newline at end of file diff --git a/src/components/DataSynchronizer.tsx b/src/components/DataSynchronizer.tsx new file mode 100644 index 0000000..6dc06be --- /dev/null +++ b/src/components/DataSynchronizer.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useAllData } from '../hooks/useFirebaseQuery'; +import { useStore } from '../store'; + +/** + * Component that synchronizes data between React Query and Zustand store + */ +export const DataSynchronizer: React.FC = () => { + const { data } = useAllData(); + const { + setTreasuryCategories, + setBeneficiaries, + setDonors, + setDonations, + setFeedingRounds, + setPayments, + setTransactions + } = useStore(); + + React.useEffect(() => { + if (data) { + // Update all store data when React Query data changes + setTreasuryCategories(data.treasury || []); + setBeneficiaries(data.beneficiaries || []); + setDonors(data.donors || []); + setDonations(data.donations || []); + setFeedingRounds(data.feedingRounds || { rounds: [], lastDoc: null }); + setPayments(data.payments || []); + setTransactions(data.transactions || []); + } + }, [data, setTreasuryCategories, setBeneficiaries, setDonors, setDonations, setFeedingRounds, setPayments, setTransactions]); + + return null; +}; \ No newline at end of file diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 1447256..ad2646d 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -3,6 +3,7 @@ import { Link, Outlet, useLocation, NavLink } from 'react-router-dom'; import { Heart, Users, Wallet, Utensils, Menu, CircleDollarSign, UserRound, CreditCard, FileText, LogOut, LayoutDashboard, DollarSign, Gift, UserCheck } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { DebugModeToggle } from './DebugModeToggle'; +import { DataSynchronizer } from './DataSynchronizer'; const Layout: React.FC = () => { const location = useLocation(); @@ -24,6 +25,9 @@ const Layout: React.FC = () => { return (
+ {/* Data Synchronizer */} + + {/* Mobile menu button */}