From e7f8f00933ce986dd089b298cddb6159f31fdb60 Mon Sep 17 00:00:00 2001 From: hrshjswniii Date: Thu, 4 Jun 2026 02:05:06 +0530 Subject: [PATCH] [Security] : Replace Client-Side Hardcoded Admin Authorization with Server-Enforced Access Control --- firestore.rules | 66 ++++++++++++++++++++++++++++++++++++ package-lock.json | 23 ++----------- src/components/Navbar.tsx | 24 +++++++++---- src/pages/AdminDashboard.tsx | 26 +++++++++----- src/pages/Resources.tsx | 28 +++++++++------ 5 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 firestore.rules diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..8b92e9b --- /dev/null +++ b/firestore.rules @@ -0,0 +1,66 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Helper function to check if the current user is an admin + function isAdmin() { + return request.auth != null && + request.auth.token.email != null && + exists(/databases/$(database)/documents/admins/$(request.auth.token.email.toLowerCase())); + } + + // Rules for the admins collection + match /admins/{email} { + allow read: if request.auth != null && (request.auth.token.email.toLowerCase() == email.toLowerCase() || isAdmin()); + allow write: if isAdmin(); + } + + // Rules for user profiles + match /profiles/{userId} { + allow read: if request.auth != null; + allow create, update: if request.auth != null && request.auth.uid == userId; + allow delete: if isAdmin(); + } + + // Rules for stories + match /stories/{storyId} { + allow read: if true; + allow create: if request.auth != null; + allow update: if request.auth != null && (request.auth.uid == resource.data.author_id || isAdmin()); + allow delete: if isAdmin(); + } + + // Rules for reactions + match /reactions/{reactionId} { + allow read: if true; + allow create, update: if request.auth != null; + allow delete: if request.auth != null && (request.auth.uid == resource.data.user_id || isAdmin()); + } + + // Rules for reports + match /reports/{reportId} { + allow read: if isAdmin(); + allow create: if request.auth != null; + allow update, delete: if isAdmin(); + } + + // Rules for ngos + match /ngos/{ngoId} { + allow read: if true; + allow write: if isAdmin(); + } + + // Rules for ngo_requests + match /ngo_requests/{requestId} { + allow read: if isAdmin(); + allow create: if request.auth != null; + allow update, delete: if isAdmin(); + } + + // Rules for testimonials + match /testimonials/{testimonialId} { + allow read: if true; + allow create: if request.auth != null; + allow update, delete: if isAdmin(); + } + } +} diff --git a/package-lock.json b/package-lock.json index 914c19b..ba470b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,7 +107,6 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -1000,7 +999,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.1.tgz", "integrity": "sha512-0O33PKrXLoIWkoOO5ByFaLjZehBctSYWnb+xJkIdx2SKP/K9l1UPFXPwASyrOIqyY3ws+7orF/1j7wI5EKzPYQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.17", "@firebase/logger": "0.4.4", @@ -1067,7 +1065,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.1.tgz", "integrity": "sha512-9VGjnY23Gc1XryoF/ABWtZVJYnaPOnjHM7dsqq9YALgKRtxI1FryvELUVkDaEIUf4In2bfkb9ZENF1S9M273Dw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.13.1", "@firebase/component": "0.6.17", @@ -1083,8 +1080,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth-compat": { "version": "0.5.27", @@ -1535,7 +1531,6 @@ "integrity": "sha512-Z4rK23xBCwgKDqmzGVMef+Vb4xso2j5Q8OG0vVL4m4fA5ZjPMYQazu8OJJC3vtQRC3SQ/Pgx/6TPNVsCd70QRw==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2234,7 +2229,6 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.10.0.tgz", "integrity": "sha512-PTigkxMdMUP6B5ISS7jMqJAKhgrhZwjprDqR1eATtFfh0OpKVNp110xiH+goeVdrJ29/4LeZJR4FaHHWstsu0A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.16" } @@ -2335,7 +2329,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.4.tgz", "integrity": "sha512-cEnx+k49knU+qdIP7rXwR6fqEXPHZs+74xFK1R0S8MgQ7v9tbePVdGxvO03n3bPympMdJWVLadARBfU4TgNHCQ==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.105.4", "@supabase/functions-js": "2.105.4", @@ -2490,7 +2483,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.8.1", "@typescript-eslint/types": "8.8.1", @@ -2724,7 +2716,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2966,7 +2957,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", @@ -3315,8 +3305,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "peer": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -3528,7 +3517,6 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5177,7 +5165,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5453,7 +5440,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5465,7 +5451,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6243,7 +6228,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6327,7 +6311,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6447,7 +6430,6 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6541,7 +6523,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c9bb4d8..cfa0ce8 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,23 +1,37 @@ import { useEffect, useState } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { Heart, Menu, X, Moon, Sun } from 'lucide-react'; -import { auth } from '../lib/firebase'; +import { auth, db } from '../lib/firebase'; import { onAuthStateChanged } from 'firebase/auth'; +import { doc, getDoc } from 'firebase/firestore'; import { toast } from 'react-hot-toast'; import { useTheme } from '../context/ThemeContext'; -const ADMIN_EMAILS = ['safevoiceforwomen@gmail.com', 'piyushydv011@gmail.com', 'aditiraj0205@gmail.com']; - export default function Navbar() { const [isMenuOpen, setIsMenuOpen] = useState(false); const [user, setUser] = useState(null); + const [isAdmin, setIsAdmin] = useState(false); const [scrolled, setScrolled] = useState(false); const navigate = useNavigate(); const location = useLocation(); const { theme, toggleTheme } = useTheme(); useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (currentUser) => setUser(currentUser)); + const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { + setUser(currentUser); + if (currentUser && currentUser.email) { + try { + const docRef = doc(db, 'admins', currentUser.email.toLowerCase()); + const docSnap = await getDoc(docRef); + setIsAdmin(docSnap.exists()); + } catch (error) { + console.error("Error checking admin status:", error); + setIsAdmin(false); + } + } else { + setIsAdmin(false); + } + }); return () => unsubscribe(); }, []); @@ -35,8 +49,6 @@ export default function Navbar() { setIsMenuOpen(false); }, [location.pathname]); - const isAdmin = user && ADMIN_EMAILS.includes(user.email || ''); - const handleSignOut = async () => { try { await auth.signOut(); diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index b90dd39..aa97d65 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -5,6 +5,7 @@ import { collection, getDocs, doc, + getDoc, addDoc, deleteDoc, serverTimestamp, @@ -18,10 +19,6 @@ import { useNavigate } from 'react-router-dom'; import { toast } from 'react-hot-toast'; import { CheckCircle, XCircle, Shield, Mail, Trash2, Flag } from 'lucide-react'; -// --- Authorization --- -// Only users signed in with these emails can see this page. -const ADMIN_EMAILS = ['safevoiceforwomen@gmail.com', 'piyushydv011@gmail.com', 'aditiraj0205@gmail.com']; - const db = getFirestore(); interface NGORequest { @@ -67,10 +64,23 @@ export default function AdminDashboard() { // Check authorization and fetch data useEffect(() => { - const unsubscribe = auth.onAuthStateChanged(user => { - if (user && ADMIN_EMAILS.includes(user.email || '')) { - setIsAuthorized(true); - fetchAllAdminData(); + const unsubscribe = auth.onAuthStateChanged(async (user) => { + if (user && user.email) { + try { + const docRef = doc(db, 'admins', user.email.toLowerCase()); + const docSnap = await getDoc(docRef); + if (docSnap.exists()) { + setIsAuthorized(true); + fetchAllAdminData(); + } else { + setIsAuthorized(false); + setLoading(false); + } + } catch (error) { + console.error("Error checking admin status:", error); + setIsAuthorized(false); + setLoading(false); + } } else { setIsAuthorized(false); setLoading(false); diff --git a/src/pages/Resources.tsx b/src/pages/Resources.tsx index a50144d..d4f8dfd 100644 --- a/src/pages/Resources.tsx +++ b/src/pages/Resources.tsx @@ -25,6 +25,7 @@ import { collection, deleteDoc, doc, + getDoc, getDocs, getFirestore, query, @@ -34,12 +35,6 @@ import { const db = getFirestore(); -const ADMIN_EMAILS = [ - "safevoiceforwomen@gmail.com", - "piyushydv011@gmail.com", - "aditiraj0205@gmail.com", -]; - interface NGO { id: string; name: string; @@ -285,6 +280,7 @@ export default function Resources() { const navigate = useNavigate(); const [user, setUser] = useState(auth.currentUser); + const [isAdmin, setIsAdmin] = useState(false); const [ngos, setNGOs] = useState([]); const [loadingNGOs, setLoadingNGOs] = useState(true); @@ -300,14 +296,24 @@ export default function Resources() { const [loadingRequest, setLoadingRequest] = useState(false); useEffect(() => { - const unsubscribe = auth.onAuthStateChanged(setUser); + const unsubscribe = auth.onAuthStateChanged(async (currentUser) => { + setUser(currentUser); + if (currentUser && currentUser.email) { + try { + const docRef = doc(db, "admins", currentUser.email.toLowerCase()); + const docSnap = await getDoc(docRef); + setIsAdmin(docSnap.exists()); + } catch (error) { + console.error("Error checking admin status:", error); + setIsAdmin(false); + } + } else { + setIsAdmin(false); + } + }); return () => unsubscribe(); }, []); - const isAdmin = !!( - user && ADMIN_EMAILS.includes(user.email || "") - ); - useEffect(() => { const fetchNGOs = async () => { setLoadingNGOs(true);