Add login gate, rate limiting, and container deploy files for company-wide use
This commit is contained in:
46
server.js
46
server.js
@@ -48,6 +48,42 @@ function sendJson(res, status, obj) {
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
/* ===== Access control =====
|
||||
* Set APP_USERNAME and APP_PASSWORD (in .env or the container environment)
|
||||
* to require a login for the whole app. Leave both unset for solo local use
|
||||
* with no login prompt. This is a simple shared-credential gate, meant to sit
|
||||
* behind the company VPN and internal network, not to replace them. */
|
||||
function checkBasicAuth(req) {
|
||||
const user = process.env.APP_USERNAME;
|
||||
const pass = process.env.APP_PASSWORD;
|
||||
if (!user || !pass) return true;
|
||||
const header = req.headers['authorization'] || '';
|
||||
const expected = 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
|
||||
return header === expected;
|
||||
}
|
||||
function requireAuth(res) {
|
||||
res.writeHead(401, {
|
||||
'WWW-Authenticate': 'Basic realm="SDE Meeting Toolkit"',
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
res.end('Authentication required.');
|
||||
}
|
||||
|
||||
/* ===== Rate limiting for the AI proxy =====
|
||||
* A shared company API key means one leaked link or one runaway script can
|
||||
* generate real cost. This caps AI draft calls per source IP. It resets if
|
||||
* the process restarts; that is an accepted tradeoff for a small internal tool. */
|
||||
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '20', 10);
|
||||
const RATE_LIMIT_WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || String(5 * 60 * 1000), 10);
|
||||
const rateLimitHits = new Map();
|
||||
function isRateLimited(ip) {
|
||||
const now = Date.now();
|
||||
const hits = (rateLimitHits.get(ip) || []).filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
||||
hits.push(now);
|
||||
rateLimitHits.set(ip, hits);
|
||||
return hits.length > RATE_LIMIT_MAX;
|
||||
}
|
||||
|
||||
function proxyToClaude(req, res) {
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
sendJson(res, 500, {
|
||||
@@ -110,7 +146,16 @@ function serveStatic(req, res) {
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (!checkBasicAuth(req)) {
|
||||
requireAuth(res);
|
||||
return;
|
||||
}
|
||||
if (req.method === 'POST' && req.url === '/api/claude') {
|
||||
const ip = req.socket.remoteAddress || 'unknown';
|
||||
if (isRateLimited(ip)) {
|
||||
sendJson(res, 429, { error: `Rate limit reached (${RATE_LIMIT_MAX} AI drafts per ${Math.round(RATE_LIMIT_WINDOW_MS / 60000)} min). Wait a bit and try again.` });
|
||||
return;
|
||||
}
|
||||
proxyToClaude(req, res);
|
||||
return;
|
||||
}
|
||||
@@ -125,4 +170,5 @@ const server = http.createServer((req, res) => {
|
||||
server.listen(PORT, () => {
|
||||
console.log(`SDE meeting toolkit running at http://localhost:${PORT}`);
|
||||
console.log(process.env.ANTHROPIC_API_KEY ? 'ANTHROPIC_API_KEY loaded: AI draft button is live.' : 'No ANTHROPIC_API_KEY found: AI draft button will return an error until you add one to .env.');
|
||||
console.log((process.env.APP_USERNAME && process.env.APP_PASSWORD) ? 'Login required: APP_USERNAME/APP_PASSWORD are set.' : 'No login required: APP_USERNAME/APP_PASSWORD are not set.');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user