Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -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']))
);
Expand All @@ -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']))
);
Expand Down Expand Up @@ -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']))
);
}
Expand All @@ -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();
}

Expand Down
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/app/community/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -258,7 +259,7 @@ export default function CommunityPage() {

{/* Description */}
<div className="prose dark:prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: selectedProject.description }} />
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(selectedProject.description) }} />
</div>

{/* Links & Skills */}
Expand Down
3 changes: 2 additions & 1 deletion src/app/u/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -854,7 +855,7 @@ function ProfileContent() {

{/* Description */}
<div className="prose dark:prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: selectedProject.description }} />
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(selectedProject.description) }} />
</div>

{/* Links & Skills */}
Expand Down
5 changes: 4 additions & 1 deletion src/components/admin/AdminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'), {
Expand Down
3 changes: 2 additions & 1 deletion src/components/profile/UserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -953,7 +954,7 @@ export default function UserProfile() {

{/* Description */}
<div className="prose dark:prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: selectedProject.description }} />
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(selectedProject.description) }} />
</div>

{/* Links & Skills */}
Expand Down
25 changes: 2 additions & 23 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
65 changes: 34 additions & 31 deletions src/context/GamificationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -109,47 +110,49 @@ 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) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};

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

Expand Down