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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ dist/
.DS_Store
Thumbs.db

# Logs
# Logs & Temporary Scratch Files
*.log
eslint_out.json
create_cors_issue.cjs
create_gssoc_labels.cjs
server/logs/
67 changes: 67 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import js from '@eslint/js';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
import globals from 'globals';

export default [
// Ignore patterns (replaces ignorePatterns in legacy config)
{
ignores: ['dist/**', 'vite.config.js'],
},

// Base JS recommended rules
js.configs.recommended,

// React + React Hooks + React Refresh (eslint-plugin-react v7 flat config)
{
files: ['**/*.{js,jsx}'],
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin,
},
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2020,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
version: 'detect',
},
},
rules: {
// React recommended rules (v7 API)
...reactPlugin.configs.recommended.rules,
// JSX runtime — disables react/react-in-jsx-scope for React 17+
...reactPlugin.configs['jsx-runtime'].rules,

// React Hooks recommended rules
...reactHooksPlugin.configs.recommended.rules,

// Project-specific overrides
'react/prop-types': 'off',
'no-unused-vars': 'warn',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],

// react-hooks v7 introduced stricter rules; downgrade to warn until
// the pre-existing patterns are refactored in a dedicated pass.
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/purity': 'warn',
'react-hooks/immutability': 'warn',
'react-hooks/refs': 'warn',
},
},
];
51 changes: 38 additions & 13 deletions firestore.rules
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Allow users to access their own private collections

// ── USERS COLLECTION ───────────────────────────────────────────────────
// Allow users to access their own user documents and subcollections (e.g. savedCode)
match /users/{userId}/{document=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}

// Allow public read access to user profiles so display name checks during signup work
match /users/{userId} {
allow read: if true;
}

// ── ROOMS COLLECTION & SUBCOLLECTIONS ──────────────────────────────────
match /rooms/{roomId} {
// ── READ ─────────────────────────────────────────────────────────────
allow read: if request.auth != null && request.auth.uid in resource.data.participantIds;
// Allow any authenticated user to read room documents (needed to inspect/join rooms)
allow read: if request.auth != null;
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Room reads now expose passwordHash, passwordSalt, and private room code.

Firestore rules are not filters. allow read: if request.auth != null grants every signed-in user the full room document, including passwordHash and passwordSalt.

The write rules on Lines 24-25, 30, 34, and 38 block clients from writing those fields, and server/routes/rooms.js Line 75 calls them "server-only hash fields". This read rule contradicts that design. Any signed-in user can now export the hash and salt of every room and run an offline attack against the scrypt hash.

The same rule also exposes code for every private room, so a user who does not know the password can read the room contents and bypass the password gate.

Restrict document reads to participants, and move the fields needed for "inspect before join" into the existing rooms/{roomId}/meta subcollection (Lines 95-98), which already has a narrower contract.

🔒 Proposed direction
-      // Allow any authenticated user to read room documents (needed to inspect/join rooms)
-      allow read: if request.auth != null;
+      // Participants only. Non-participants read rooms/{roomId}/meta to inspect a room
+      // before joining. Password hash/salt must never reach a client.
+      allow read: if request.auth != null
+                  && (
+                    request.auth.uid == resource.data.createdBy
+                    || request.auth.uid in resource.data.participantIds
+                  );

Write the join-time metadata (name, passwordProtected) to rooms/{roomId}/meta from the server when the room is created.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Allow any authenticated user to read room documents (needed to inspect/join rooms)
allow read: if request.auth != null;
// Participants only. Non-participants read rooms/{roomId}/meta to inspect a room
// before joining. Password hash/salt must never reach a client.
allow read: if request.auth != null
&& (
request.auth.uid == resource.data.createdBy
|| request.auth.uid in resource.data.participantIds
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@firestore.rules` around lines 19 - 20, Replace the broad authenticated-user
read rule for room documents with a participant-only condition, and ensure
private room credentials and server-only hash fields are never exposed through
that document. Update the room-creation flow to write the join-time metadata
(`name` and `passwordProtected`) from the server into the existing
`rooms/{roomId}/meta` subcollection, preserving the narrower metadata read
contract used for inspection before joining.


// ── WRITE ────────────────────────────────────────────────────────────
allow create: if request.auth != null
Expand Down Expand Up @@ -40,28 +49,45 @@ service cloud.firestore {
allow delete: if request.auth != null
&& request.auth.uid == resource.data.createdBy;

// ── ROOM SUBCOLLECTIONS ──────────────────────────────────────────────

// Subcollection for collaborative voting
match /votes/{voteId} {
allow read: if request.auth != null && request.auth.uid in get(/databases/$(database)/documents/rooms/$(roomId)).data.participants;
allow read: if request.auth != null;
allow create, update: if request.auth != null;
allow delete: if request.auth != null
&& (
resource.data.createdBy == request.auth.uid ||
get(/databases/$(database)/documents/rooms/$(roomId)).data.createdBy == request.auth.uid
);
allow delete: if request.auth != null;
}

// Subcollection for collaborative cursors
match /cursors/{userId} {
allow read: if request.auth != null
&& request.auth.uid in get(/databases/$(database)/documents/rooms/$(roomId)).data.participantIds;
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == userId;
}

// Subcollection for chat messages
match /messages/{messageId} {
allow read, create: if request.auth != null
&& request.auth.uid in get(/databases/$(database)/documents/rooms/$(roomId)).data.participantIds;
allow read, create: if request.auth != null;
}

// Subcollections for WebRTC Video / Voice Calls & Signaling
match /calls/{callId} {
allow read, write: if request.auth != null;
}
match /calls/{callId}/{document=**} {
allow read, write: if request.auth != null;
}

match /voice_participants/{userId} {
allow read, write: if request.auth != null;
}

match /signals/{signalId} {
allow read, create, delete: if request.auth != null;
}

// Subcollection for Whiteboard Drawings
match /drawings/{drawingId} {
allow read, create, delete: if request.auth != null;
}
}

Expand All @@ -77,4 +103,3 @@ service cloud.firestore {
}
}
}

Loading
Loading