Extract memind_adm admin server, add local dev tooling, and remove image-generation.
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>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
// 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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user