b0f5d6a51c
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
79 lines
2.7 KiB
JavaScript
79 lines
2.7 KiB
JavaScript
// admin-guard.mjs
|
|
//
|
|
// Pure, testable network-guard helpers for the memind_adm consoles.
|
|
//
|
|
// Each back-office console (platform super-admin, plaza operations) can be
|
|
// pinned to an allowlist of request hosts and/or client IPs, independently.
|
|
// This lets the super-admin surface be locked down harder than moderation
|
|
// while both still live in one process — and the same knobs keep working
|
|
// unchanged once the consoles are split into separate processes.
|
|
|
|
export function parseList(value) {
|
|
return String(value ?? '')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function hostAllowed(reqHost, allowedHosts) {
|
|
if (!allowedHosts.length) return true;
|
|
const hostname = String(reqHost ?? '').split(':')[0].toLowerCase();
|
|
return allowedHosts.some((h) => h.toLowerCase() === hostname);
|
|
}
|
|
|
|
function ipv4ToInt(ip) {
|
|
const parts = ip.split('.');
|
|
if (parts.length !== 4) return null;
|
|
let acc = 0;
|
|
for (const part of parts) {
|
|
if (!/^\d{1,3}$/.test(part)) return null;
|
|
const n = Number(part);
|
|
if (n < 0 || n > 255) return null;
|
|
acc = (acc * 256 + n) >>> 0;
|
|
}
|
|
return acc >>> 0;
|
|
}
|
|
|
|
// Strip an IPv4-mapped IPv6 prefix so "::ffff:127.0.0.1" matches "127.0.0.1".
|
|
function normalizeIp(ip) {
|
|
return String(ip ?? '').replace(/^::ffff:/i, '');
|
|
}
|
|
|
|
export function ipMatches(clientIp, rule) {
|
|
const ip = normalizeIp(clientIp);
|
|
if (rule.includes('/')) {
|
|
const [base, bitsStr] = rule.split('/');
|
|
const bits = Number(bitsStr);
|
|
const ipInt = ipv4ToInt(ip);
|
|
const baseInt = ipv4ToInt(base);
|
|
if (ipInt === null || baseInt === null) return false;
|
|
if (!Number.isInteger(bits) || bits < 0 || bits > 32) return false;
|
|
if (bits === 0) return true;
|
|
const mask = bits === 32 ? 0xffffffff : (~((1 << (32 - bits)) - 1)) >>> 0;
|
|
return (ipInt & mask) === (baseInt & mask);
|
|
}
|
|
return ip === normalizeIp(rule);
|
|
}
|
|
|
|
export function ipAllowed(clientIp, ipAllowlist) {
|
|
if (!ipAllowlist.length) return true;
|
|
return ipAllowlist.some((rule) => ipMatches(clientIp, rule));
|
|
}
|
|
|
|
/**
|
|
* Build an Express middleware enforcing host + IP allowlists for one console.
|
|
* Returns null when neither list is configured (no-op — common in dev).
|
|
* @param {object} opts
|
|
* @param {string[]} [opts.allowedHosts]
|
|
* @param {string[]} [opts.ipAllowlist]
|
|
* @param {(res, req, reason: 'host'|'ip') => unknown} opts.onDeny
|
|
*/
|
|
export function buildNetworkGuard({ allowedHosts = [], ipAllowlist = [], onDeny }) {
|
|
if (!allowedHosts.length && !ipAllowlist.length) return null;
|
|
return (req, res, next) => {
|
|
if (!hostAllowed(req.get('host'), allowedHosts)) return onDeny(res, req, 'host');
|
|
if (!ipAllowed(req.ip, ipAllowlist)) return onDeny(res, req, 'ip');
|
|
return next();
|
|
};
|
|
}
|