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
4 changes: 2 additions & 2 deletions apps/dashboard-api/src/__tests__/auth.controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ describe('auth.controller', () => {

expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('User not found');
expect(next.mock.calls[0][0].message).toBe('Invalid email or password');
});

test('returns 400 on invalid password', async () => {
Expand All @@ -256,7 +256,7 @@ describe('auth.controller', () => {

expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('Invalid password');
expect(next.mock.calls[0][0].message).toBe('Invalid email or password');
});

test('returns 400 on Zod validation error (missing password)', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ jest.mock('@urbackend/common', () => ({
AppError,
Project: {
findOne: jest.fn()
}
},
getProjectAccessQuery: jest.fn((userId) => ({ $or: [{ owner: userId }, { "members.user": userId }] }))
}));

const { Project } = require('@urbackend/common');
Expand Down Expand Up @@ -48,7 +49,7 @@ describe('loadProjectForAdmin Middleware', () => {

await loadProjectForAdmin(req, res, next);

expect(Project.findOne).toHaveBeenCalledWith({ _id: 'proj123', owner: 'user123' });
expect(Project.findOne).toHaveBeenCalledWith({ _id: 'proj123', $or: [{ owner: 'user123' }, { "members.user": 'user123' }] });
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith(expect.any(AppError));
const error = next.mock.calls[0][0];
Expand All @@ -63,7 +64,7 @@ describe('loadProjectForAdmin Middleware', () => {

await loadProjectForAdmin(req, res, next);

expect(Project.findOne).toHaveBeenCalledWith({ _id: 'proj123', owner: 'user123' });
expect(Project.findOne).toHaveBeenCalledWith({ _id: 'proj123', $or: [{ owner: 'user123' }, { "members.user": 'user123' }] });
expect(req.project).toEqual(mockProject);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ jest.mock('@urbackend/common', () => ({
this.statusCode = statusCode;
this.isOperational = true;
}
}
},
getProjectAccessQuery: jest.fn((userId) => ({ $or: [{ owner: userId }, { "members.user": userId }] }))
}));

const { deleteRow, recoverRow } = require('../controllers/project.controller');
Expand Down Expand Up @@ -142,7 +143,13 @@ describe('Soft Delete in dashboard project.controller', () => {

await recoverRow(req, res, next);

expect(mockFindOne).toHaveBeenCalledWith({ _id: 'proj_1', owner: 'user_1' });
expect(mockFindOne).toHaveBeenCalledWith({
_id: 'proj_1',
$or: [
{ owner: 'user_1' },
{ 'members.user': 'user_1' }
]
});
expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
expect.objectContaining({
_id: '507f1f77bcf86cd799439011',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@ jest.mock('../middlewares/planEnforcement', () => ({
checkByodGate: jest.fn((_req, _res, next) => next()),
checkWebhookGate: jest.fn((_req, _res, next) => next()),
checkMailTemplatesGate: jest.fn((_req, _res, next) => next()),
checkMemberLimit: jest.fn((_req, _res, next) => next()),
}));

jest.mock('@urbackend/common', () => ({
verifyEmail: jest.fn((_req, _res, next) => next()),
checkAuthEnabled: jest.fn((_req, _res, next) => next()),
loadProjectForAdmin: jest.fn((_req, _res, next) => next()),
}));

const mockAuthZ = jest.fn((req, res, next) => next());
jest.mock('../middlewares/authorizeProject', () => {
const m = jest.fn(() => mockAuthZ);
m.middleware = mockAuthZ;
return m;
});

jest.mock('../controllers/userAuth.controller', () => ({
createAdminUser: jest.fn((_req, res) => res.json({ ok: true })),
resetPassword: jest.fn((_req, res) => res.json({ ok: true })),
Expand Down Expand Up @@ -76,6 +83,10 @@ jest.mock('../controllers/project.controller', () => {
manageContacts: jest.fn(ok),
deleteContact: jest.fn(ok),
sendMarketingBroadcast: jest.fn(ok),
getMembers: jest.fn(ok),
inviteMember: jest.fn(ok),
updateMemberRole: jest.fn(ok),
removeMember: jest.fn(ok),
};
});

Expand All @@ -84,7 +95,8 @@ const request = require('supertest');
const projectsRouter = require('../routes/projects');
const projectController = require('../controllers/project.controller');
const authMiddleware = require('../middlewares/authMiddleware');
const { verifyEmail, loadProjectForAdmin } = require('@urbackend/common');
const { verifyEmail } = require('@urbackend/common');
const authorizeProject = require('../middlewares/authorizeProject');

let app;

Expand Down Expand Up @@ -112,7 +124,7 @@ describe('projects storage presigned routes', () => {
expect(res.status).toBe(200);
expect(authMiddleware).toHaveBeenCalled();
expect(verifyEmail).toHaveBeenCalled();
expect(loadProjectForAdmin).toHaveBeenCalled();
expect(authorizeProject.middleware).toHaveBeenCalled();
expect(projectController.requestUpload).toHaveBeenCalledTimes(1);
});

Expand All @@ -124,7 +136,7 @@ describe('projects storage presigned routes', () => {
expect(res.status).toBe(200);
expect(authMiddleware).toHaveBeenCalled();
expect(verifyEmail).toHaveBeenCalled();
expect(loadProjectForAdmin).toHaveBeenCalled();
expect(authorizeProject.middleware).toHaveBeenCalled();
expect(projectController.confirmUpload).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jest.mock('@urbackend/common', () => {
return normalized;
}),
AppError,
getProjectAccessQuery: jest.fn((userId) => ({ owner: userId })),
__mockStorageFrom: mockStorageFrom,
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jest.mock('@urbackend/common', () => {
});
}
},
getProjectAccessQuery: jest.fn((userId) => ({ owner: userId })),
};
});

Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard-api/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const billingRoute = require('./routes/billing');
const eventsRoute = require('./routes/events');
const adminMetricsRoute = require('./routes/admin.metrics');
const aiRoute = require('./routes/ai.routes');
const invitationsRoute = require('./routes/invitations');

app.use('/api/auth', authRoute);
app.use('/api/projects', dashboardLimiter, projectRoute);
Expand All @@ -116,6 +117,7 @@ app.use('/api/analytics', dashboardLimiter, analyticsRoute);
app.use('/api/billing', billingRoute);
app.use('/api/events', dashboardLimiter, eventsRoute);
app.use('/api/admin/metrics', dashboardLimiter, adminMetricsRoute);
app.use('/api/invitations', dashboardLimiter, invitationsRoute);



Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard-api/src/controllers/ai.controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { Project } = require('@urbackend/common/src/models');
const { forwardToPythonService } = require('../utils/internalPythonClient');
const { AppError, ApiResponse } = require('@urbackend/common');
const { AppError, ApiResponse, getProjectAccessQuery } = require('@urbackend/common');

/**
* Controller to handle AI Query Builder requests.
Expand Down Expand Up @@ -35,7 +35,7 @@ const queryBuilder = async (req, res, next) => {

// 1. Fetch the project and specifically the requested collection schema
const project = await Project.findOne(
{ _id: projectId, owner: req.user._id, "collections.name": safeCollectionName },
{ ...getProjectAccessQuery(req.user._id), _id: projectId, "collections.name": safeCollectionName },
{ "collections.$": 1 }
);

Expand Down
13 changes: 4 additions & 9 deletions apps/dashboard-api/src/controllers/analytics.controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Project, Log, Developer, Webhook, getConnection, resolveEffectivePlan, getPlanLimits, PlatformEvent, DeveloperActivity, AppError, ApiResponse } = require("@urbackend/common");
const { Project, Log, Developer, Webhook, getConnection, resolveEffectivePlan, getPlanLimits, PlatformEvent, DeveloperActivity, AppError, ApiResponse, getProjectAccessQuery } = require("@urbackend/common");
const mongoose = require("mongoose");

/**
Expand All @@ -12,12 +12,7 @@ module.exports.getGlobalStats = async (req, res, next) => {
const [stats, dev] = await Promise.all([
Project.aggregate([
{
$match: {
$or: [
{ owner: user_id },
{ owner: userId }
]
}
$match: { owner: userId },
},
{
$group: {
Expand Down Expand Up @@ -90,7 +85,7 @@ module.exports.getGlobalStats = async (req, res, next) => {
module.exports.getRecentActivity = async (req, res, next) => {
try {
const userId = req.user._id;
const projectIds = await Project.find({ owner: userId }).distinct("_id");
const projectIds = await Project.find(getProjectAccessQuery(userId)).distinct("_id");

const logs = await Log.find({ projectId: { $in: projectIds } })
.sort({ timestamp: -1 })
Expand Down Expand Up @@ -271,7 +266,7 @@ module.exports.getNorthStar = async (req, res, next) => {
const sevenDaysAgo = new Date();
sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7);

// Projects owned by this developer
// Projects owned by this developer (North Star should be owner-only)
const allProjects = await Project.find({ owner: developerId }).select('_id name').lean();
const projectIds = allProjects.map((p) => p._id);
const totalProjects = projectIds.length;
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard-api/src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,10 @@ module.exports.login = async (req, res, next) => {
const { email, password } = loginSchema.parse(req.body);

const dev = await Developer.findOne({ email: email.toLowerCase().trim() }).select('+password');
if (!dev) return next(new AppError(400, "User not found"));
if (!dev) return next(new AppError(400, "Invalid email or password"));

const validPass = await bcrypt.compare(password, dev.password);
if (!validPass) return next(new AppError(400, "Invalid password"));
if (!validPass) return next(new AppError(400, "Invalid email or password"));

await sendTokenResponse(dev, 200, res);
} catch (err) {
Expand Down
19 changes: 12 additions & 7 deletions apps/dashboard-api/src/controllers/dbExport.controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { AppError, ApiResponse } = require('@urbackend/common');
const { AppError, ApiResponse, getProjectRole } = require('@urbackend/common');
const { Developer } = require('@urbackend/common');
const { Project } = require('@urbackend/common');
const { exportQueue } = require('@urbackend/common');
Expand All @@ -19,24 +19,29 @@ module.exports.dbExportHandler = async (req, res, next) => {
await setProjectById(projectId, project);
}

if (project.owner.toString() !== userId.toString()) {
return next(new AppError(403, "Access denied. You are not the owner of this project."));
if (!getProjectRole(project, userId)) {
return next(new AppError(403, "Access denied. You are not a member of this project."));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if (!project.collections.some(c => c.name === collectionName)) {
return next(new AppError(404, "Collection not found in project."));
}

const developer = await Developer.findById(userId).select('email plan').lean();

const developer = await Developer.findById(userId).select('email').lean();
if (!developer) {
return next(new AppError(404, "Authenticated developer not found."));
}
const { email, plan = 'free' } = developer;
const { email } = developer;

console.log(`[Dashboard API] Received export request for collection ${collectionName} in project ${projectId} from user ${userId} (${email})`);


// Derive maxExports from project owner's plan
const projectOwner = await Developer.findById(project.owner).select('plan').lean();
if (!projectOwner) {
return next(new AppError(404, "Project owner not found."));
}
const { plan = 'free' } = projectOwner;
const maxExports = plan === 'pro' ? 5 : 1;
const today = new Date().toISOString().split('T')[0];
const key = `project:${projectId}:export_limit:${today}`;
Expand Down
Loading
Loading