-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathusageGate.js
More file actions
133 lines (111 loc) · 4.48 KB
/
Copy pathusageGate.js
File metadata and controls
133 lines (111 loc) · 4.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
const {
redis,
Developer,
resolveEffectivePlan,
getPlanLimits,
getDeveloperPlanCache,
setDeveloperPlanCache,
AppError,
sanitizeObjectId,
getConnection,
getCompiledModel,
getDayKey,
DEFAULT_DAILY_TTL_SECONDS,
incrWithTtlAtomic
} = require('@urbackend/common');
/**
* Resolves the plan context for the current project's owner.
* Uses Redis cache to avoid DB hits on every public API request.
*/
async function resolveDeveloperPlanContext(req) {
const rawOwner = req.project.owner;
const developerId = (rawOwner && typeof rawOwner === 'object' && rawOwner._id)
? rawOwner._id.toString()
: rawOwner.toString();
// Sanitize to prevent NoSQL injection if owner was somehow corrupted
const cleanDeveloperId = sanitizeObjectId(developerId);
if (!cleanDeveloperId) return { plan: 'free', legacyLimits: {} };
// Try cache first
let cached = await getDeveloperPlanCache(cleanDeveloperId);
if (cached) return cached;
// Cache miss: Load from DB
const developer = await Developer.findById(cleanDeveloperId).select('plan planExpiresAt maxProjects maxCollections').lean();
const context = {
plan: developer?.plan || 'free',
planExpiresAt: developer?.planExpiresAt || null,
legacyLimits: {
maxProjects: developer?.maxProjects ?? null,
maxCollections: developer?.maxCollections ?? null
}
};
// Store in cache (5 mins)
await setDeveloperPlanCache(cleanDeveloperId, context);
return context;
}
/**
* Middleware to check daily request limits and per-minute spikes.
*/
exports.checkUsageLimits = async (req, res, next) => {
try {
if (!req.project) return next();
const planContext = await resolveDeveloperPlanContext(req);
const effectivePlan = resolveEffectivePlan(planContext);
const limits = getPlanLimits({
plan: effectivePlan,
customLimits: req.project.customLimits,
legacyLimits: planContext.legacyLimits
});
req.planLimits = limits;
const minKey = `project:usage:min:${req.project._id}:${new Date().toISOString().substring(0, 16)}`;
const minCount = await incrWithTtlAtomic(redis, minKey, 65);
if (limits.reqPerMinute !== -1 && minCount > limits.reqPerMinute) {
return next(new AppError(429, 'Rate limit exceeded (per minute). Please slow down or upgrade your plan.'));
}
const day = getDayKey();
const reqCountKey = `project:usage:req:count:${req.project._id}:${day}`;
const newDailyCount = await incrWithTtlAtomic(redis, reqCountKey, DEFAULT_DAILY_TTL_SECONDS);
if (limits.reqPerDay !== -1 && newDailyCount > limits.reqPerDay) {
await redis.decr(reqCountKey);
return next(new AppError(429, 'Daily request limit reached. Upgrade your plan to increase limits.'));
}
req._dailyCountIncremented = true;
next();
} catch (err) {
console.error("Usage limit check failed:", err);
next();
}
};
/**
* Middleware to enforce Auth User limits (e.g., 200 for Free).
* Applied to signup and social auth routes.
*/
exports.checkAuthUsersLimit = async (req, res, next) => {
try {
if (!req.project) return next();
// 1. Resolve limits if not already attached by checkUsageLimits
if (!req.planLimits) {
const planContext = await resolveDeveloperPlanContext(req);
const effectivePlan = resolveEffectivePlan(planContext);
req.planLimits = getPlanLimits({
plan: effectivePlan,
customLimits: req.project.customLimits,
legacyLimits: planContext.legacyLimits
});
}
const limit = req.planLimits.authUsersLimit;
if (limit === -1) return next(); // Unlimited
// 2. Count existing users
const usersCol = req.project.collections.find(c => c.name === 'users');
if (!usersCol) return next();
const connection = await getConnection(req.project._id);
const Model = getCompiledModel(connection, usersCol, req.project._id, req.project.resources.db.isExternal);
const count = await Model.countDocuments();
if (count >= limit) {
return next(new AppError(403, `User limit reached (${limit}). Please upgrade your plan to allow more users.`));
}
next();
} catch (err) {
console.error("Auth user limit check failed:", err);
next();
}
};