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
5 changes: 3 additions & 2 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ app.use(

app.use(cors(corsOptions));

const isProduction = process.env.NODE_ENV === 'production';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
windowMs: isProduction ? 15 * 60 * 1000 : 60 * 1000,
max: isProduction ? 100 : 600,
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: {
Expand Down
33 changes: 9 additions & 24 deletions backend/middleware/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
const admin = require('firebase-admin');

const { admin, getFirestoreAdmin } = require('../services/firebaseAdmin');

if (!admin.apps.length) {
try {

if (process.env.GCP_SERVICE_ACCOUNT_KEY) {

let serviceAccount = process.env.GCP_SERVICE_ACCOUNT_KEY;
if (typeof serviceAccount === 'string') {
try {
serviceAccount = JSON.parse(serviceAccount);
} catch (e) {
console.error("Failed to parse GCP_SERVICE_ACCOUNT_KEY JSON", e);
}
}
// Prefer service-account init from shared helper.
getFirestoreAdmin();

console.log("Initializing Firebase Admin with project_id:", serviceAccount.project_id);

admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
} else {

console.log("Initializing Firebase Admin with default credentials");
// Fallback for environments that only provide ADC.
if (!admin.apps.length) {
try {
admin.initializeApp();
console.log('Initializing Firebase Admin with default credentials');
} catch (error) {
console.warn('Firebase Admin failed to initialize:', error.message);
}
} catch (error) {
console.warn("Firebase Admin failed to initialize:", error.message);
}
}

Expand Down
23 changes: 23 additions & 0 deletions backend/routes/teamRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ const User = require('../models/User');
const Team = require('../models/Team');
const { normalizeDoc, normalizeDocs } = require('../utils/normalize');
const { paginateArray, setPaginationHeaders } = require('../utils/pagination');
const {
upsertTeamSnapshot,
addMemberToTeam,
removeMemberFromTeam,
transferTeamOwnership,
deleteTeamSnapshot,
} = require('../services/teamFirebaseSync');

const runSync = async (label, fn) => {
try {
await fn();
} catch (error) {
console.error(`[TeamSync] ${label} failed:`, error?.message || error);
}
};


const generateInviteCode = async () => {
Expand Down Expand Up @@ -73,6 +88,7 @@ router.post('/create', verifyToken, async (req, res) => {
// Add team to user's memberships
const memberships = [...(user.teamMemberships || []), teamObj.id];
await User.updateOne({ uid }, { $set: { teamMemberships: memberships } });
await runSync('create-team', () => upsertTeamSnapshot(teamObj));

if (initialInvites && Array.isArray(initialInvites) && initialInvites.length > 0) {
const { sendZYNCEmail } = require('../services/mailer');
Expand Down Expand Up @@ -131,6 +147,8 @@ router.post('/join', verifyToken, async (req, res) => {
const teamId = team._id.toString();
const memberships = [...(user.teamMemberships || []), teamId];
await User.updateOne({ uid }, { $set: { teamMemberships: memberships } });
await runSync('join-team-add-member', () => addMemberToTeam(teamId, uid));
await runSync('join-team-upsert', () => upsertTeamSnapshot(updatedTeam));

res.status(200).json(normalizeDoc(updatedTeam));

Expand Down Expand Up @@ -164,6 +182,7 @@ router.delete('/:teamId', verifyToken, async (req, res) => {
}

await Team.findByIdAndDelete(teamId);
await runSync('delete-team', () => deleteTeamSnapshot(teamId, team.members, team.ownerId));

res.status(200).json({ message: 'Team deleted successfully' });
} catch (error) {
Expand Down Expand Up @@ -201,6 +220,7 @@ router.delete('/:teamId/members/:memberUid', verifyToken, async (req, res) => {
{ $set: { teamMemberships: (member.teamMemberships || []).filter(id => id !== teamId) } }
);
}
await runSync('remove-member', () => removeMemberFromTeam(teamId, memberUid));

res.status(200).json({ message: 'Member removed successfully' });
} catch (error) {
Expand Down Expand Up @@ -275,6 +295,7 @@ router.post('/:teamId/leave', verifyToken, async (req, res) => {
{ $set: { teamMemberships: (user.teamMemberships || []).filter(id => id !== teamId) } }
);
}
await runSync('leave-team', () => removeMemberFromTeam(teamId, uid));

res.status(200).json({ message: 'Left team successfully' });
} catch (error) {
Expand Down Expand Up @@ -339,6 +360,7 @@ router.patch('/:teamId/transfer-ownership', verifyToken, async (req, res) => {
}

await Team.findByIdAndUpdate(teamId, { $set: { ownerId: newOwnerId } });
await runSync('transfer-ownership', () => transferTeamOwnership(teamId, uid, newOwnerId));

res.status(200).json({ message: 'Ownership transferred successfully' });
} catch (error) {
Expand Down Expand Up @@ -373,6 +395,7 @@ router.patch('/:teamId/name', verifyToken, async (req, res) => {
{ $set: { name: nextName } },
{ returnDocument: 'after', lean: true }
);
await runSync('rename-team', () => upsertTeamSnapshot(updated));

res.status(200).json(normalizeDoc(updated));
} catch (error) {
Expand Down
23 changes: 13 additions & 10 deletions backend/routes/userRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,19 @@ router.post('/sync', verifyToken, async (req, res) => {
}

try {
const existingUser = await User.findOne({ uid }).lean();
const safeEmail = email || existingUser?.email;
if (!safeEmail) {
return res.status(400).json({ message: 'Email is required to sync user profile' });
}

let finalDisplayName = displayName;
if (!finalDisplayName && email) {
finalDisplayName = email.split('@')[0];
if (!finalDisplayName && safeEmail) {
finalDisplayName = safeEmail.split('@')[0];
}

const updateData = {
email,
email: safeEmail,
status: 'online',
lastSeen: new Date()
};
Expand All @@ -146,7 +152,7 @@ router.post('/sync', verifyToken, async (req, res) => {
if (lastName) updateData.lastName = lastName;
if (timezone) updateData.timezone = timezone;

const result = await User.findOneAndUpdate(
const user = await User.findOneAndUpdate(
{ uid },
{
$set: updateData,
Expand All @@ -165,23 +171,20 @@ router.post('/sync', verifyToken, async (req, res) => {
},
{
upsert: true,
returnDocument: 'after',
new: true,
lean: true,
includeResultMetadata: true,
rawResult: true,
setDefaultsOnInsert: true
}
);

const user = result?.value || result;
const isNewUserInsert = wasUserInsertedFromUpsertResult(result);
const isNewUserInsert = !existingUser;

if (isNewUserInsert) {
console.log(`[SYNC] Sending welcome notifications for newly inserted user: ${uid} (${email})`);
// Fire-and-forget: don't block response on external notifications
dispatchNewUserNotifications({
displayName: finalDisplayName,
email,
email: safeEmail,
uid
});
}
Expand Down
52 changes: 52 additions & 0 deletions backend/services/firebaseAdmin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const admin = require('firebase-admin');

let firestoreInstance = null;
let attemptedInit = false;

const parseServiceAccount = () => {
const raw = process.env.GCP_SERVICE_ACCOUNT_KEY;
if (!raw) return null;

let normalized = raw.trim();
if (
(normalized.startsWith("'") && normalized.endsWith("'")) ||
(normalized.startsWith('"') && normalized.endsWith('"'))
) {
normalized = normalized.slice(1, -1);
}

const parsed = JSON.parse(normalized);
if (parsed?.private_key && typeof parsed.private_key === 'string') {
parsed.private_key = parsed.private_key.replace(/\\n/g, '\n');
}
return parsed;
};

const getFirestoreAdmin = () => {
if (firestoreInstance) return firestoreInstance;
if (attemptedInit) return null;

attemptedInit = true;
try {
const serviceAccount = parseServiceAccount();
if (!serviceAccount) {
console.warn('[FirebaseAdmin] GCP_SERVICE_ACCOUNT_KEY not set; Firestore sync disabled.');
return null;
}

if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
projectId: serviceAccount.project_id || process.env.VITE_FIREBASE_PROJECT_ID,
});
}

firestoreInstance = admin.firestore();
return firestoreInstance;
} catch (error) {
console.error('[FirebaseAdmin] Failed to initialize firebase-admin:', error.message);
return null;
}
};

module.exports = { admin, getFirestoreAdmin };
Loading
Loading