Add login gate, rate limiting, and container deploy files for company-wide use

This commit is contained in:
2026-08-20 15:06:37 -07:00
parent e5c01003f8
commit e77cd64d88
6 changed files with 123 additions and 1 deletions

View File

@@ -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.');
});