Skip to content
Open
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
12 changes: 10 additions & 2 deletions backend/routes/userRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const router = express.Router();
const verifyToken = require('../middleware/authMiddleware');
const User = require('../models/User');
const Team = require('../models/Team');
const admin = require('firebase-admin');
const { encrypt } = require('../utils/encryption');
const { escapeRegExp } = require('../utils/regexUtils');
const { sendZyncEmail } = require('../services/mailer');
Expand Down Expand Up @@ -458,8 +459,15 @@ router.post('/delete/confirm', verifyToken, async (req, res) => {
);

await User.findOneAndDelete({ uid });
// TODO: also trigger Firebase Authentication deletion via Admin SDK if possible,
// but the frontend handles the client-side firebase auth deletion.

// Trigger Firebase Authentication deletion
try {
await admin.auth().deleteUser(uid);
console.log(`Successfully deleted user ${uid} from Firebase Auth`);
} catch (firebaseError) {
console.warn(`Failed to delete user ${uid} from Firebase Auth:`, firebaseError.message);
Comment on lines +466 to +468
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Logging user IDs in plain text may expose sensitive information in production logs. Consider using a sanitized identifier or removing this log statement in production environments.

Suggested change
console.log(`Successfully deleted user ${uid} from Firebase Auth`);
} catch (firebaseError) {
console.warn(`Failed to delete user ${uid} from Firebase Auth:`, firebaseError.message);
console.log('Successfully deleted user from Firebase Auth');
} catch (firebaseError) {
console.warn('Failed to delete user from Firebase Auth:', firebaseError.message);

Copilot uses AI. Check for mistakes.
// Proceed without error as DB deletion was successful
Comment on lines +468 to +469
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Silent failures of Firebase deletion could lead to orphaned accounts accumulating over time. Consider implementing retry logic or logging this to a monitoring service for tracking failed deletions that may need manual cleanup.

Copilot uses AI. Check for mistakes.
}
Comment on lines +462 to +470
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Orphaned Firebase account can re-create the MongoDB user via /sync.

If admin.auth().deleteUser(uid) fails (line 465), the user receives a 200 success response but their Firebase Auth account survives. On next login, the /sync endpoint would re-create the MongoDB user document, effectively un-deleting the account.

Consider either:

  1. Attempting the Firebase deletion before the MongoDB deletion (so you can abort cleanly on failure), or
  2. Retrying the Firebase deletion (e.g., via a background job or dead-letter queue) to ensure eventual consistency.

Option 1 is simpler and still safe — if Firebase deletion succeeds but the subsequent MongoDB deletion fails, the user simply can't log in anymore (which is the desired end-state anyway).

💡 Suggested reorder: delete Firebase first
-    await User.findOneAndDelete({ uid });
-
-    // Trigger Firebase Authentication deletion
-    try {
-      await admin.auth().deleteUser(uid);
-      console.log(`Successfully deleted user ${uid} from Firebase Auth`);
-    } catch (firebaseError) {
-      console.warn(`Failed to delete user ${uid} from Firebase Auth:`, firebaseError.message);
-      // Proceed without error as DB deletion was successful
-    }
+    // Delete Firebase Auth user first to prevent orphan re-creation via /sync
+    try {
+      await admin.auth().deleteUser(uid);
+    } catch (firebaseError) {
+      console.error(`Failed to delete user ${uid} from Firebase Auth:`, firebaseError.message);
+      return res.status(500).json({ message: 'Failed to delete account. Please try again.' });
+    }
+
+    await User.findOneAndDelete({ uid });
🤖 Prompt for AI Agents
In `@backend/routes/userRoutes.js` around lines 462 - 470, Current flow deletes
the MongoDB user first then calls admin.auth().deleteUser(uid), which can leave
an orphaned Firebase account that later recreates the MongoDB document via
/sync; reorder operations so you call admin.auth().deleteUser(uid) before
performing the MongoDB deletion (e.g., move the admin.auth().deleteUser(uid)
try/catch to run prior to the DB delete) and if Firebase delete fails abort with
an error response; alternatively implement a retry/background job (dead-letter
queue) around admin.auth().deleteUser(uid) to ensure eventual success — update
the code paths that reference admin.auth().deleteUser, the DB delete function
(the Mongo delete routine), and any error handling to reflect the new ordering
and retries.


res.status(200).json({ message: 'User deleted' });
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions electron/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ afterAll(() => {
* Extend the global namespace with custom test types.
*/
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Vi {
interface Assertion {
toExist(): void;
Expand Down
75 changes: 73 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"commit": "cz",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
"clean": "rimraf dist dist_electron node_modules coverage",
"typecheck": "tsc --noEmit",
"typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.electron.json --noEmit",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/notes/NoteEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ note, user, onUpdate, className
// Simplified: Just use the text content of the block where cursor is.
if (block && block.content) {
// Block content is array of InlineContent. Join text.
// @ts-ignore
// @ts-expect-error BlockNote types mismatch
text = block.content.map(c => c.text || "").join("");
}

Expand Down
1 change: 1 addition & 0 deletions src/components/ui/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Command = React.forwardRef<
));
Command.displayName = CommandPrimitive.displayName;

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface CommandDialogProps extends DialogProps {}

const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
Expand Down
1 change: 1 addition & 0 deletions src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";

import { cn } from "@/lib/utils";

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/MobileView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const MobileView = () => {
const renderContent = () => {
switch (activeTab) {
case "Home":
return currentUser ? <DashboardView /> : null; // Reusing Desktop DashboardView for now, check responsiveness later
return currentUser ? <DashboardView currentUser={currentUser} /> : null; // Reusing Desktop DashboardView for now, check responsiveness later
case "Projects":
return currentUser ? (
<div className="p-4">
Expand Down
10 changes: 5 additions & 5 deletions src/components/views/PeopleView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ const PeopleView = ({ users: propUsers, userStatuses, onChat, onMessages, isPrev
{effectiveCollapsed && <div className="h-px bg-border/50 w-8 mx-auto my-2" />}

<div className="space-y-1">
{myTeams.map((team) => (
{myTeams.map((team: any) => (
<div
key={team._id}
onClick={() => {
Expand Down Expand Up @@ -353,7 +353,7 @@ const PeopleView = ({ users: propUsers, userStatuses, onChat, onMessages, isPrev
<DialogDescription>Add or remove people from your close friends list.</DialogDescription>
</DialogHeader>
<div className="space-y-2 max-h-[60vh] overflow-y-auto mt-2 pr-1">
{allKnownUsers.filter(u => u.uid !== currentUser?.uid).map(user => {
{allKnownUsers.filter((u: any) => u.uid !== currentUser?.uid).map((user: any) => {
const isSelected = localCloseFriendsIds.includes(user.uid);
return (
<div key={user.uid} className="flex items-center justify-between p-2 rounded-lg hover:bg-secondary/20 border border-transparent hover:border-border/30 transition-colors">
Expand Down Expand Up @@ -389,7 +389,7 @@ const PeopleView = ({ users: propUsers, userStatuses, onChat, onMessages, isPrev
{effectiveCollapsed && <div className="h-px bg-border/50 w-8 mx-auto my-2" />}

<div className="space-y-1">
{closeFriendUsers.map((friend) => (
{closeFriendUsers.map((friend: any) => (
<div
key={friend.uid}
className={cn(
Expand Down Expand Up @@ -468,7 +468,7 @@ const PeopleView = ({ users: propUsers, userStatuses, onChat, onMessages, isPrev
if (currentUser?.uid === teamInfo.ownerId) {
ownerUser = currentUser;
} else {
ownerUser = users.find(u => u.uid === teamInfo.ownerId);
ownerUser = users.find((u: any) => u.uid === teamInfo.ownerId);
}
}
// Fallback to current user if logic fails or data missing (safe default)
Expand Down Expand Up @@ -619,7 +619,7 @@ const PeopleView = ({ users: propUsers, userStatuses, onChat, onMessages, isPrev
</div>
) : (
<div key={teamInfo?._id || 'members-list'} className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{users.map((user, index) => {
{users.map((user: any, index: number) => {
const statusData = !isPreview && userStatuses[user.uid]
? userStatuses[user.uid]
: { status: user.status, lastSeen: user.lastSeen };
Expand Down
3 changes: 3 additions & 0 deletions src/components/workspace/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ interface KanbanBoardProps {
onUpdateTask: (stepId: string, taskId: string, updates: any) => void;
users: any[];
isOwner?: boolean;
currentUser?: any;
readOnly?: boolean;
onDeleteTask?: (stepId: string, taskId: string) => void;
}

const COLUMN_MAPPING: Record<string, string> = {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ describe("utils.ts", () => {
});

test("should handle conditional classes", () => {
expect(cn("btn", true && "btn-active", false && "btn-hidden")).toBe("btn btn-active");
const isBtnActive = Boolean(true);
const isBtnHidden = Boolean(false);
expect(cn("btn", isBtnActive && "btn-active", isBtnHidden && "btn-hidden")).toBe("btn btn-active");
});

test("should handle objects", () => {
Expand Down
3 changes: 2 additions & 1 deletion src/pages/ProjectDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ArrowLeft, Loader2, CheckCircle2, Circle, Server, Layout, Database, Share2, Plus, GripVertical, GitCommit, ExternalLink, Kanban, Trash2, Github, Bot, MoreVertical, Settings, MessageSquare, Wrench } from "lucide-react";
import { ArrowLeft, Loader2, CheckCircle2, Circle, Server, Layout, Database, Share2, Plus, GripVertical, GitCommit, ExternalLink, Kanban, Trash2, Github, Bot, MoreVertical, Settings, MessageSquare, Wrench, FolderKanban } from "lucide-react";
import { API_BASE_URL, getFullUrl } from "@/lib/utils";
import { auth, db } from "@/lib/firebase";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
Expand Down Expand Up @@ -69,6 +69,7 @@ interface Task {
status: "Pending" | "In Progress" | "Active" | "Completed";
assignedTo?: string;
assignedToName?: string;
createdBy?: string;
commitInfo?: {
message: string;
url: string;
Expand Down
12 changes: 7 additions & 5 deletions tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"emitDeclarationOnly": true,
"noEmit": true,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check root tsconfig.json for project references
echo "=== Root tsconfig.json ==="
cat tsconfig.json

echo ""
echo "=== package.json typecheck script ==="
grep -A 5 "typecheck" package.json || echo "typecheck not found in package.json"

echo ""
echo "=== tsconfig.app.json content ==="
cat tsconfig.app.json

Repository: prem22k/Zync

Length of output: 2522


CI is broken: composite: true is required by a project reference, but conflicts with noEmit: true.

The pipeline fails with: Referenced project 'tsconfig.app.json' must have setting "composite": true. However, composite and noEmit are mutually exclusive in TypeScript — you cannot set both. When composite: true is set, TypeScript must generate declaration files, requiring noEmit to be false.

The root tsconfig.json contains "references": [{ "path": "./tsconfig.app.json" }, ...], which triggers validation of project references. Since the typecheck script in package.json already invokes tsc -p explicitly for each config, these project references are unnecessary. Remove the entire "references" array from the root tsconfig.json to unblock CI.

🤖 Prompt for AI Agents
In `@tsconfig.app.json` at line 17, Remove the root tsconfig.json "references"
array to eliminate the project reference validation that requires "composite":
true (which conflicts with "noEmit": true in tsconfig.app.json); open the root
tsconfig.json, delete the entire "references": [...] entry so the typecheck
script (which runs tsc -p per-config) remains the authority and CI no longer
demands composite output from tsconfig.app.json.

"jsx": "react-jsx",
"composite": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
Expand Down Expand Up @@ -52,5 +50,9 @@
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts"
],
"exclude": [
"src/**/*.test.ts",
"src/**/*.test.tsx"
]
}
1 change: 0 additions & 1 deletion tsconfig.electron.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
Expand Down
4 changes: 1 addition & 3 deletions tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": false,
"emitDeclarationOnly": true,
"noEmit": true,
"types": [
"node"
]
Expand Down
Loading