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);
});
};