diff --git a/firestore.rules b/firestore.rules index ec3fbfd9..9e80a145 100644 --- a/firestore.rules +++ b/firestore.rules @@ -39,7 +39,6 @@ service cloud.firestore { request.auth.token.email == adminId || request.auth.uid == adminId || isSuperAdmin() || - (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['followers', 'points'])) || (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['followers'])) || (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['following'])) ); @@ -65,7 +64,7 @@ service cloud.firestore { allow update: if request.auth != null && ( request.auth.uid == userId || isSuperAdmin() || - (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['followers', 'points'])) || + (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['followers'])) || (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['following'])) || (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['starredResources'])) ); @@ -117,7 +116,7 @@ service cloud.firestore { match /leaderboard/{userId} { allow read: if true; allow write: if (request.auth != null && request.auth.uid == userId) || isSuperAdmin(); // Owner or Super Admin can write - allow update: if request.auth != null && ( + allow update: if request.auth != null && request.auth.uid == userId && ( (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['points'])) ); } @@ -136,13 +135,13 @@ service cloud.firestore { // Superadmin Keys collection match /superadmin_keys/{keyId} { - allow read: if true; // Public read so signup page can verify + allow read: if isSuperAdmin(); // Prevent public read allow write: if isSuperAdmin(); // Only super admin can change the key } // Admin Keys collection match /admin_keys/{keyId} { - allow read: if true; // Public read so admin login can verify + allow read: if isSuperAdmin(); // Prevent public read allow write: if isSuperAdmin(); } diff --git a/package-lock.json b/package-lock.json index 184fed76..0ba99caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.2", "canvas-confetti": "^1.9.4", + "dompurify": "^3.4.5", "firebase": "^12.7.0", "firebase-admin": "^13.6.0", "framer-motion": "^12.23.26", @@ -29,6 +30,7 @@ }, "devDependencies": { "@types/canvas-confetti": "^1.9.0", + "@types/dompurify": "^3.0.5", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -4459,6 +4461,16 @@ "@types/ms": "*" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/draco3d": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", @@ -4728,6 +4740,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -7592,6 +7611,15 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz", + "integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", diff --git a/package.json b/package.json index e8e05ecf..43542eee 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.2", "canvas-confetti": "^1.9.4", + "dompurify": "^3.4.5", "firebase": "^12.7.0", "firebase-admin": "^13.6.0", "framer-motion": "^12.23.26", @@ -31,6 +32,7 @@ }, "devDependencies": { "@types/canvas-confetti": "^1.9.0", + "@types/dompurify": "^3.0.5", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index 65641ae7..4e327dd1 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -10,6 +10,7 @@ import { getEmbedUrl } from '@/lib/utils'; import { useRouter } from 'next/navigation'; import CreateDiscussionModal from '@/components/community/CreateDiscussionModal'; import ProjectCard from '@/components/projects/ProjectCard'; +import DOMPurify from 'dompurify'; export default function CommunityPage() { const { user } = useAuth(); @@ -258,7 +259,7 @@ export default function CommunityPage() { {/* Description */}
-
+
{/* Links & Skills */} diff --git a/src/app/u/client.tsx b/src/app/u/client.tsx index 09a6266c..22f7b45e 100644 --- a/src/app/u/client.tsx +++ b/src/app/u/client.tsx @@ -11,6 +11,7 @@ import styles from '@/components/profile/Profile.module.css'; import Rewards from '@/components/profile/Rewards'; import FollowButton from '@/components/profile/FollowButton'; import LoginHeatmap from '@/components/profile/LoginHeatmap'; +import DOMPurify from 'dompurify'; import { useSearchParams } from 'next/navigation'; import { useAuth } from '@/context/AuthContext'; import { GIT_FALLBACK_STATS } from '@/lib/github'; @@ -854,7 +855,7 @@ function ProfileContent() { {/* Description */}
-
+
{/* Links & Skills */} diff --git a/src/components/admin/AdminDashboard.tsx b/src/components/admin/AdminDashboard.tsx index a3ff4089..a81c7abc 100644 --- a/src/components/admin/AdminDashboard.tsx +++ b/src/components/admin/AdminDashboard.tsx @@ -1037,7 +1037,10 @@ export default function AdminDashboard({ initialAuth = false }: AdminDashboardPr // Rotate Key if not auto-login (fresh login) if (!isAuto) { - const newAdminKey = `devpath-admin-${Math.random().toString(36).substring(2, 10)}${Math.random().toString(36).substring(2, 6)}`; + const array = new Uint8Array(32); + crypto.getRandomValues(array); + const randomHex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('').substring(0, 16); + const newAdminKey = `devpath-admin-${randomHex}`; // Update Key await setDoc(doc(db, 'superadmin_keys', 'config'), { diff --git a/src/components/profile/UserProfile.tsx b/src/components/profile/UserProfile.tsx index b82166e5..671ed15c 100644 --- a/src/components/profile/UserProfile.tsx +++ b/src/components/profile/UserProfile.tsx @@ -15,6 +15,7 @@ import Rewards from '@/components/profile/Rewards'; import FollowButton from '@/components/profile/FollowButton'; import LoginHeatmap from '@/components/profile/LoginHeatmap'; import ReactMarkdown from 'react-markdown'; +import DOMPurify from 'dompurify'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import { collection, query, where, orderBy, getDocs, doc, updateDoc, arrayUnion, increment } from 'firebase/firestore'; @@ -953,7 +954,7 @@ export default function UserProfile() { {/* Description */}
-
+
{/* Links & Skills */} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 7d8bbc39..cd75e11f 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -112,29 +112,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { if (firebaseUser) { try { - // SUPER ADMIN BYPASS - if (firebaseUser.email === SUPER_ADMIN_EMAIL) { - const superAdminUser: User = { - uid: firebaseUser.uid, - email: firebaseUser.email, - name: "Super Admin", - photoURL: firebaseUser.photoURL, - role: 'admin', - // Minimal required fields to prevent crashes - privacySettings: { - showMobile: false, - showLocation: false, - showEmail: false, - showProjects: false, - showRewards: false, - isPublic: false, - showInCommunity: false - } - }; - setUser(superAdminUser); - setIsLoading(false); - return; - } + // REMOVED SUPER ADMIN BYPASS: The super admin now goes through the standard listener flow. + // This ensures their session is tracked and they are validated securely via Firestore `admins` collection. let role: 'admin' | 'member' = 'member'; let userData: any = { diff --git a/src/context/GamificationContext.tsx b/src/context/GamificationContext.tsx index acbe95fe..ff102327 100644 --- a/src/context/GamificationContext.tsx +++ b/src/context/GamificationContext.tsx @@ -3,6 +3,7 @@ import React, { createContext, useContext, useState } from 'react'; import { Trophy, Zap, ArrowUpCircle, X } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +import { useAuth } from '@/context/AuthContext'; export type NotificationType = 'xp' | 'achievement' | 'level-up'; @@ -109,9 +110,10 @@ const ToastMessage = ({ n, removeNotification }: { n: any, removeNotification: ( }; export function GamificationProvider({ children }: { children: React.ReactNode }) { - const [xp, setXp] = useState(125400); + const { user, updateUserProfile } = useAuth(); const [notifications, setNotifications] = useState<{ id: number; title: string; subtitle: string; type: NotificationType }[]>([]); + const xp = user?.points || 0; const level = Math.floor(Math.sqrt(xp / 100)); const removeNotification = (id: number) => { @@ -119,37 +121,38 @@ export function GamificationProvider({ children }: { children: React.ReactNode } }; const addXp = (amount: number, reason: string, type: NotificationType = 'xp') => { - setXp(prev => { - const newXp = prev + amount; - const currentLevel = Math.floor(Math.sqrt(prev / 100)); - const newLevel = Math.floor(Math.sqrt(newXp / 100)); - - const baseId = Date.now(); - const newNotifications = [{ - id: baseId, - title: type === 'achievement' ? 'Achievement Unlocked' : `+${amount} XP`, - subtitle: reason, - type - }]; - - if (newLevel > currentLevel) { - newNotifications.push({ - id: baseId + 1, - title: `Level Up!`, - subtitle: `You reached Level ${newLevel}`, - type: 'level-up' - }); - } - - setNotifications(prevNotifs => [...prevNotifs, ...newNotifications]); - - newNotifications.forEach(n => { - setTimeout(() => { - removeNotification(n.id); - }, 4000); + if (!user) return; // Must be logged in + + const newXp = xp + amount; + const currentLevel = Math.floor(Math.sqrt(xp / 100)); + const newLevel = Math.floor(Math.sqrt(newXp / 100)); + + // Persist XP to Firestore securely + updateUserProfile({ points: newXp }).catch(err => console.error("Failed to update XP", err)); + + const baseId = Date.now(); + const newNotifications = [{ + id: baseId, + title: type === 'achievement' ? 'Achievement Unlocked' : `+${amount} XP`, + subtitle: reason, + type + }]; + + if (newLevel > currentLevel) { + newNotifications.push({ + id: baseId + 1, + title: `Level Up!`, + subtitle: `You reached Level ${newLevel}`, + type: 'level-up' }); - - return newXp; + } + + setNotifications(prevNotifs => [...prevNotifs, ...newNotifications]); + + newNotifications.forEach(n => { + setTimeout(() => { + removeNotification(n.id); + }, 4000); }); };