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
2 changes: 2 additions & 0 deletions packages/common/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ const { validateData, validateUpdateData } = require("./utils/validateData");
const sessionManager = require("./utils/session.manager");
const planLimits = require("./utils/planLimits");
const AppError = require("./utils/AppError");
const ApiResponse = require("./utils/ApiResponse");
const { checkLockout, recordFailedAttempt, clearLockout } = require("./utils/loginLockout");
const { dispatchWebhooks } = require("./utils/webhookDispatcher");
const { getDayKey, getMonthKey, getEndOfMonthTtlSeconds, incrWithTtlAtomic } = require("./utils/usageCounter");
Expand Down Expand Up @@ -200,6 +201,7 @@ module.exports = {
...sessionManager,
...planLimits,
AppError,
ApiResponse,
getPresignedUploadUrl,
verifyUploadedFile,
PlatformEvent,
Expand Down
17 changes: 5 additions & 12 deletions packages/common/src/middleware/checkAuthEnabled.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
// FUNCTION - CHECK AUTH ENABLED (MIDDLEWARE)
const AppError = require('../utils/AppError');

module.exports = (req, res, next) => {
const project = req.project;

if (!project.isAuthEnabled) {
return res.status(403).json({
error: "Authentication service is disabled",
message: "Please enable Auth in the urBackend dashboard for this project to use this endpoint."
});
return next(new AppError(403, "Please enable Auth in the urBackend dashboard for this project to use this endpoint.", "Authentication service is disabled"));
}

const usersCollection = project.collections?.find(c => c.name === 'users');

if (!usersCollection) {
return res.status(403).json({
error: "User Schema Missing",
message: "Authentication is enabled, but the 'users' collection schema has not been defined. Please create a 'users' collection in the dashboard to define your custom user fields."
});
return next(new AppError(403, "Authentication is enabled, but the 'users' collection schema has not been defined. Please create a 'users' collection in the dashboard to define your custom user fields.", "User Schema Missing"));
}

const hasEmail = usersCollection.model.find(f => f.key === 'email' && f.type === 'String' && f.required);
const hasPassword = usersCollection.model.find(f => f.key === 'password' && f.type === 'String' && f.required);

if (!hasEmail || !hasPassword) {
return res.status(422).json({
error: "Invalid Users Schema",
message: "The 'users' collection is missing required 'email' and 'password' string fields. Please fix the schema in the dashboard."
});
return next(new AppError(422, "The 'users' collection is missing required 'email' and 'password' string fields. Please fix the schema in the dashboard.", "Invalid Users Schema"));
}

req.usersSchema = usersCollection.model;
Expand Down
8 changes: 5 additions & 3 deletions packages/common/src/middleware/loadProjectForAdmin.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
// FUNCTION - LOAD PROJECT FOR ADMIN (MIDDLEWARE)
const Project = require('../models/Project');
const AppError = require('../utils/AppError');

module.exports = async (req, res, next) => {
try {
const { projectId } = req.params;
if (!projectId) return res.status(400).json({ error: "Project ID is required" });
if (!projectId) return next(new AppError(400, "Project ID is required"));

const project = await Project.findOne({ _id: projectId, owner: req.user._id });
if (!project) {
return res.status(404).json({ error: "Project not found or access denied" });
return next(new AppError(404, "Project not found or access denied"));
}

req.project = project;
next();
} catch (err) {
res.status(500).json({ error: err.message });
console.error("loadProjectForAdmin Error:", err);
next(new AppError(500, "Internal Server Error"));
}
};
18 changes: 16 additions & 2 deletions packages/common/src/middleware/standardizeApiResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,24 @@ module.exports = (req, res, next) => {
}

if (code >= 400) {
let errorTitle = 'Error';
let errorMessage = toErrorMessage(body);

if (body && typeof body === 'object') {
if (body.error && typeof body.error === 'string') errorTitle = body.error;
if (body.message && typeof body.message === 'string') errorMessage = body.message;

// If it only had error and no message, use error as message
if (body.error && !body.message) {
errorTitle = 'Error';
errorMessage = typeof body.error === 'string' ? body.error : safeStringify(body.error);
}
}

return originalJson({
success: false,
error: toErrorMessage(body),
code,
error: errorTitle,
message: errorMessage
});
}

Expand Down
4 changes: 3 additions & 1 deletion packages/common/src/middleware/verifyEmail.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const AppError = require('../utils/AppError');

module.exports = function (req, res, next) {
const { isVerified } = req.user;
if (!isVerified) return res.status(401).json({ error: "Email not verified" });
if (!isVerified) return next(new AppError(401, "Email not verified"));
next();
};
17 changes: 16 additions & 1 deletion packages/common/src/queues/publicEmailQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ const DECR_IF_EXISTS_SCRIPT = `if redis.call('EXISTS', KEYS[1]) == 1 then return
const publicEmailQueue = new Queue('public-email-queue', { connection });

let worker = null;
const resetPublicEmailWorker = async () => {
if (!worker) return;
try {
if (typeof worker.removeAllListeners === 'function') {
worker.removeAllListeners();
}
if (typeof worker.close === 'function') {
await worker.close();
}
} catch (_) {
// Best-effort cleanup for test/runtime shutdown paths.
} finally {
worker = null;
}
};

const initPublicEmailWorker = () => {
if (worker) return worker;
Expand Down Expand Up @@ -117,4 +132,4 @@ const initPublicEmailWorker = () => {
return worker;
};

module.exports = { publicEmailQueue, initPublicEmailWorker };
module.exports = { publicEmailQueue, initPublicEmailWorker, resetPublicEmailWorker };
26 changes: 26 additions & 0 deletions packages/common/src/utils/ApiResponse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Standard utility for consistent API success responses across the monorepo.
* Ensures structure: { success: true, data: {}, message: "" }
*/
class ApiResponse {
constructor(data = {}, message = "Success") {
this.data = data;
this.message = message;
this.success = true;
}

/**
* Sends a standardized success response
* @param {Object} res - Express response object
* @param {number} statusCode - HTTP status code
*/
send(res, statusCode = 200) {
return res.status(statusCode).json({
success: this.success,
data: this.data,
message: this.message
});
}
}

module.exports = ApiResponse;
3 changes: 2 additions & 1 deletion packages/common/src/utils/AppError.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
* Ensures consistent error structure: { success: false, data: {}, message: "" }
*/
class AppError extends Error {
constructor(statusCode, message) {
constructor(statusCode, message, error = null) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.error = error || (statusCode >= 500 ? "Internal Server Error" : "Error");
this.isOperational = true;

Error.captureStackTrace(this, this.constructor);
Expand Down
5 changes: 4 additions & 1 deletion packages/common/src/utils/emailService.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ const formatFromAddress = (email_address) => {
return FALLBACK_FROM_ADDRESS;
}

// simplified the sender formatting logic and removed the regex based parsing to avoid the CodeQL warning
// If the address already includes a custom name format (e.g., "Name <email@domain.com>")
if (trimmed.includes('<') && trimmed.endsWith('>')) {
return trimmed;
}

return `urBackend <${trimmed}>`;
};
Expand Down
Loading