263 lines
9.6 KiB
JavaScript
263 lines
9.6 KiB
JavaScript
'use strict';
|
|
|
|
const
|
|
env = require('../helpers/env.js'),
|
|
{ verifyAsync, decodeAsync } = require('../helpers/jwt_async.js'),
|
|
{ TokenExpiredError } = require('jsonwebtoken'),
|
|
utils = require('../helpers/utils.js'),
|
|
cache = require('../helpers/mem_cache.js'),
|
|
subUtil = require('../helpers/subscription_util.js'),
|
|
{ Errors, Fields } = require('../helpers/constants.js'),
|
|
{ AppAuthError, AppMembershipError, AppParamError } = require('../helpers/app_error.js'),
|
|
{ SubType, SubFields } = require('../model/subscription.js'),
|
|
Vehicle = require('../model/vehicle.js'),
|
|
bcrypt = require('bcryptjs'),
|
|
ObjectId = require('mongodb').ObjectId;
|
|
|
|
const USE_SUBSCRIPTION = env.ENABLE_SUBSCRIPTION;
|
|
|
|
function getRoutePath(req) {
|
|
return req && req.baseUrl + req.path;
|
|
}
|
|
function isSecuredRoute(routePath, method) {
|
|
const nonSecurePaths = [
|
|
{ path: '/login', method: 'ALL' },
|
|
{ path: '/siteVer', method: 'ALL' },
|
|
{ path: '/mailPwdReset', method: 'POST' },
|
|
{ path: '/resetPassword', method: 'ALL' },
|
|
{ path: '/signup', method: 'ALL' },
|
|
{ path: '/api/partners', method: 'GET', exact: true }, // Allow unauthenticated GET /api/partners only (not subroutes)
|
|
{ path: '/exists', method: 'POST' },
|
|
{ path: '/countries', method: 'GET' },
|
|
{ path: '/testAuth', method: 'ALL' },
|
|
{ path: '/stPmtWH_EP', method: 'POST' }, // Stripe webhook endpoint - authenticated via signature verification
|
|
{ path: '/api/v1/', method: 'ALL' } // Public data-export API — authenticated via X-API-Key header instead
|
|
];
|
|
if (env.INV_IMG_VIR_DIR) {
|
|
nonSecurePaths.push({ path: env.INV_IMG_VIR_DIR, method: 'ALL' });
|
|
}
|
|
|
|
return !nonSecurePaths.some(
|
|
(route) => {
|
|
const methodMatches = (route.method === 'ALL') || (method && method === route.method);
|
|
const pathMatches = route.exact ? routePath === route.path : routePath.includes(route.path);
|
|
return pathMatches && methodMatches;
|
|
}
|
|
);
|
|
}
|
|
|
|
/** Check on every request and authenticate and authorize users. This middleware must be call first to ensure user authentication and related user session data */
|
|
async function checkUser(req, res, next) {
|
|
// console.log(req.url);
|
|
if (!isSecuredRoute(getRoutePath(req), req.method)) return next && next();
|
|
|
|
const token = req.headers.authorization ? req.headers.authorization.split(' ') : null;
|
|
if (!token || token.length !== 2 || token[0].toLowerCase() !== 'bearer') AppAuthError.throw();
|
|
|
|
const bearer = token[1];
|
|
try {
|
|
|
|
const decodedToken = await verifyAsync(bearer, env.TOKEN_SECRET);
|
|
|
|
/* Setup for current logged in user */
|
|
req.uid = decodedToken.uid; // Set req User id
|
|
if (!ObjectId.isValid(req.uid)) AppAuthError.throw();
|
|
|
|
req.ut = decodedToken.ut; // Set req User type
|
|
|
|
|
|
// Rebuild the in-memory cache for this user in case of server restarted or the user's cache has expired
|
|
const userInfo = cache.get(req.uid);
|
|
if (userInfo && userInfo[Fields.MARKED_DELETE]) {
|
|
AppAuthError.throw();
|
|
} else if (!userInfo || cache.isExpired(req.uid, env.MAX_SESSION_SECS)) {
|
|
const curUserInfo = await cache.loadUser(req.uid);
|
|
|
|
if (!curUserInfo && curUserInfo[Fields.MARKED_DELETE]) {
|
|
AppAuthError.throw();
|
|
}
|
|
|
|
req.userInfo = curUserInfo;
|
|
return next && next();
|
|
} else {
|
|
req.userInfo = userInfo;
|
|
return next && next();
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof TokenExpiredError) {
|
|
const decodedPkg = await decodeAsync(bearer);
|
|
// Handle the case the user logout when token already expired => should still allow clearing temp data
|
|
if (req.path.endsWith('/clearTempData')) {
|
|
req.uid = decodedPkg.uid;
|
|
if (!ObjectId.isValid(req.uid)) AppAuthError.throw();
|
|
|
|
req.ut = decodedPkg.ut;
|
|
|
|
return next && next();
|
|
}
|
|
AppAuthError.throw(Errors.TOKEN_EXPIRED);
|
|
} else {
|
|
AppAuthError.throw();
|
|
}
|
|
}
|
|
}
|
|
|
|
function getUserInfo(req) {
|
|
if (!req) AppParamError.throw();
|
|
|
|
const userInfo = req.userInfo;
|
|
|
|
if (!USE_SUBSCRIPTION) return userInfo; // Temporarily before merging subscription management feature
|
|
|
|
if (!userInfo || !userInfo.membership || utils.isEmptyArray(userInfo.membership?.subscriptions))
|
|
AppMembershipError.throw();
|
|
|
|
return userInfo;
|
|
}
|
|
/** Check for require any subscription. It requires to be called after checkUser middleware */
|
|
|
|
async function checkRqAnySubscription(req, res, next) {
|
|
if (!USE_SUBSCRIPTION) return next && next(); // Temporarily before merging subscription management feature
|
|
|
|
const routeUrl = getRoutePath(req);
|
|
if (!isSecuredRoute(routeUrl, req.method)) return next && next();
|
|
try {
|
|
getUserInfo(req);
|
|
return next && next();
|
|
} catch (err) {
|
|
return next && next(err);
|
|
}
|
|
}
|
|
/** Check for require any subscription. It requires to be called after checkUser middleware */
|
|
|
|
async function checkRqPkgSubscription(req, res, next) {
|
|
if (!USE_SUBSCRIPTION) return next && next(); // Temporarily before merging subscription management feature
|
|
|
|
const routeUrl = getRoutePath(req);
|
|
if (!isSecuredRoute(routeUrl, req.method)) return next && next();
|
|
try {
|
|
const userInfo = getUserInfo(req);
|
|
|
|
if (utils.isEmptyArray(userInfo?.membership?.subscriptions) || !(userInfo.membership.subscriptions.some(sub => sub.type === SubType.PACKAGE))) {
|
|
return AppMembershipError.throw(Errors.PKG_SUBSCRIPTION_NOT_FOUND);
|
|
}
|
|
return next && next();
|
|
} catch (err) {
|
|
return next && next(err);
|
|
}
|
|
}
|
|
// async function checkRqTrackingSubscription(req, res, next) {
|
|
// // TODO: Handle the check for subscription for addon(s) by name.
|
|
// }
|
|
|
|
async function checkACsLimits(userInfo) {
|
|
if (!USE_SUBSCRIPTION) return true; // Temporarily before merging subscription management feature
|
|
|
|
if (!userInfo) AppAuthError.throw();
|
|
|
|
const pkgSub = subUtil.getPkgSubfromUserInfo(userInfo);
|
|
|
|
const numOfAC = await Vehicle.countDocuments({ parent: userInfo.puid, [Fields.MARKED_DELETE]: { $in: [false, null] }, pkgActive: true });
|
|
// Check for customer-specific override first, fallback to package limit
|
|
const maxVehicles = userInfo.membership?.customLimits?.maxVehicles
|
|
?? subUtil.getSubMetaField(pkgSub, SubFields.MAX_VEHICLES)
|
|
?? 0;
|
|
|
|
if (numOfAC && numOfAC >= maxVehicles) {
|
|
AppMembershipError.throw(Errors.REACHED_VEHICLES_LIMIT);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function checkUsageLimits(userInfo) {
|
|
if (!USE_SUBSCRIPTION) return true; // Temporarily before merging subscription management feature
|
|
|
|
if (!userInfo) AppAuthError.throw();
|
|
|
|
const pkgSub = subUtil.getPkgSubfromUserInfo(userInfo);
|
|
|
|
// Check and skip paid migrated users which have trial subscription and migratedDate is not null
|
|
if (pkgSub.type === SubType.TRIAL && userInfo?.migratedDate) return true;
|
|
|
|
// Check for customer-specific override first, fallback to package limit
|
|
const priceMaxAcres = userInfo.membership?.customLimits?.maxAcres
|
|
?? subUtil.getSubMetaField(pkgSub, SubFields.MAX_ACRES)
|
|
?? 0;
|
|
if (priceMaxAcres) {
|
|
const ttSprArea = await subUtil.calcTotalAreaByUser(ObjectId(userInfo.puid), pkgSub.periodStart, pkgSub.periodEnd);
|
|
if (ttSprArea && utils.haToAcre(ttSprArea) >= priceMaxAcres) {
|
|
AppMembershipError.throw(Errors.REACHED_AREA_LIMIT);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function checkRqACsLimits(req, res, next) {
|
|
if (!USE_SUBSCRIPTION) return next && next(); // Temporarily before merging subscription management feature
|
|
|
|
const userInfo = getUserInfo(req);
|
|
await checkACsLimits(userInfo);
|
|
|
|
return next && (next());
|
|
}
|
|
|
|
async function checkRqUsageLimits(req, res, next) {
|
|
if (!USE_SUBSCRIPTION) return next && next(); // Temporarily before merging subscription management feature
|
|
|
|
const userInfo = getUserInfo(req);
|
|
await checkUsageLimits(userInfo);
|
|
|
|
return next && next();
|
|
}
|
|
|
|
|
|
/**
|
|
* API-Key middleware for the public data-export API (/api/v1/ routes).
|
|
*
|
|
* Reads the X-API-Key header, looks up a candidate key by its prefix (first 8 chars),
|
|
* then bcrypt-compares the full key. On success, sets req.uid identical to checkUser
|
|
* so all existing controller ownership filters work without modification.
|
|
*
|
|
* lastUsedAt is updated fire-and-forget to avoid adding latency to every request.
|
|
*/
|
|
async function checkApiKey(req, res, next) {
|
|
const ApiKey = require('../model/api_key'); // lazy require to avoid circular dependency risk
|
|
const rawKey = req.headers['x-api-key'];
|
|
if (!rawKey || rawKey.length < 8) return AppAuthError.throw();
|
|
|
|
const prefix = rawKey.substring(0, 8);
|
|
// Find active keys matching this prefix (should be at most one, prefix is not a unique index
|
|
// to avoid leaking timing info about key existence)
|
|
const candidates = await ApiKey.find({ prefix, active: true }).limit(5).lean();
|
|
if (!candidates.length) AppAuthError.throw();
|
|
|
|
let matched = null;
|
|
for (const candidate of candidates) {
|
|
const ok = await bcrypt.compare(rawKey, candidate.keyHash);
|
|
if (ok) { matched = candidate; break; }
|
|
}
|
|
if (!matched) AppAuthError.throw();
|
|
|
|
// Mirror what checkUser sets — controllers need req.uid and nothing else
|
|
req.uid = matched.owner.toString();
|
|
req.apiKeyId = matched._id;
|
|
|
|
// Fire-and-forget lastUsedAt + requestCount update (do not await — avoids adding DB latency to request)
|
|
ApiKey.updateOne({ _id: matched._id }, { $set: { lastUsedAt: new Date() }, $inc: { requestCount: 1 } }).catch(() => {});
|
|
|
|
// Load owner's userInfo from cache (same path as checkUser)
|
|
const userInfo = cache.get(req.uid);
|
|
if (userInfo && !userInfo[Fields.MARKED_DELETE]) {
|
|
req.userInfo = userInfo;
|
|
} else {
|
|
req.userInfo = await cache.loadUser(req.uid);
|
|
}
|
|
|
|
return next && next();
|
|
}
|
|
|
|
module.exports = {
|
|
isSecuredRoute, getUserInfo, checkUser, checkApiKey, checkACsLimits, checkUsageLimits,
|
|
checkRqAnySubscription, checkRqPkgSubscription, checkRqACsLimits, checkRqUsageLimits,
|
|
};
|