Skip to content

Commit 72ba39d

Browse files
committed
fix: make redis quota reservation atomic and standardize error responses
1 parent e7a1fd1 commit 72ba39d

1 file changed

Lines changed: 20 additions & 10 deletions

File tree

‎apps/public-api/src/controllers/mail.controller.js‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ const reserveMonthlyMailSlot = async (projectId, limit) => {
3030
const ttlSeconds = getEndOfMonthTtlSeconds(now);
3131
const key = getMailCountKey(projectId, monthKey);
3232

33-
const count = await redis.incr(key);
34-
if (count === 1) {
35-
await redis.expire(key, ttlSeconds);
36-
}
33+
const luaScript = `
34+
local current = redis.call("INCR", KEYS[1])
35+
if current == 1 then
36+
redis.call("EXPIRE", KEYS[1], ARGV[1])
37+
end
38+
return current
39+
`;
40+
const count = await redis.eval(luaScript, 1, key, ttlSeconds);
3741

3842
if (count > limit) {
3943
await redis.decr(key);
@@ -51,20 +55,22 @@ module.exports.sendMail = async (req, res) => {
5155
try {
5256
if (req.keyRole !== "secret") {
5357
return res.status(403).json({
54-
error: "Forbidden. This action requires a Secret Key (sk_live_...).",
58+
success: false,
59+
data: {},
60+
message: "Forbidden. This action requires a Secret Key (sk_live_...).",
5561
});
5662
}
5763

5864
const { to, subject, html, text } = sendMailSchema.parse(req.body || {});
5965
const projectId = req.project?._id;
6066

6167
if (!projectId) {
62-
return res.status(401).json({ error: "Project context missing." });
68+
return res.status(401).json({ success: false, data: {}, message: "Project context missing." });
6369
}
6470

6571
const project = await loadProjectMailConfig(projectId);
6672
if (!project) {
67-
return res.status(404).json({ error: "Project not found." });
73+
return res.status(404).json({ success: false, data: {}, message: "Project not found." });
6874
}
6975

7076
const encryptedByokKey =
@@ -79,7 +85,7 @@ module.exports.sendMail = async (req, res) => {
7985
: process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY;
8086

8187
if (!clientKey) {
82-
return res.status(500).json({ error: "Resend API key is not configured." });
88+
return res.status(500).json({ success: false, data: {}, message: "Resend API key is not configured." });
8389
}
8490

8591
const limit = getMonthlyMailLimit(req.project);
@@ -125,12 +131,16 @@ module.exports.sendMail = async (req, res) => {
125131

126132
if (err instanceof z.ZodError) {
127133
return res.status(400).json({
128-
error: err.issues?.[0]?.message || "Invalid mail payload.",
134+
success: false,
135+
data: {},
136+
message: err.issues?.[0]?.message || "Invalid mail payload.",
129137
});
130138
}
131139

132140
return res.status(err.statusCode || 500).json({
133-
error: err.message || "Failed to send mail.",
141+
success: false,
142+
data: {},
143+
message: err.message || "Failed to send mail.",
134144
...(typeof err.limit === "number" ? { limit: err.limit } : {}),
135145
});
136146
}

0 commit comments

Comments
 (0)