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

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.env
node_modules
*.log
README.md
QUICKSTART.md

View File

@@ -1,2 +1,14 @@
## Copy this file to .env and fill in your key. Do not commit .env.
## Copy this file to .env and fill in real values. Do not commit .env.
# Required for the AI draft button.
ANTHROPIC_API_KEY=your-anthropic-api-key-here
# Required for company deployment. Leave both blank for solo local use with
# no login prompt. Set both to require a login for anyone reaching this tool.
APP_USERNAME=
APP_PASSWORD=
# Optional. Caps AI draft calls per source IP, to limit cost from the shared
# ANTHROPIC_API_KEY. Defaults: 20 requests per 5 minutes.
RATE_LIMIT_MAX=20
RATE_LIMIT_WINDOW_MS=300000

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM node:22-alpine
WORKDIR /app
# No dependencies to install: server.js uses only Node's built-in modules.
COPY package.json ./
COPY server.js ./
COPY tools ./tools
COPY skills ./skills
ENV PORT=5173
EXPOSE 5173
CMD ["node", "server.js"]

View File

@@ -126,6 +126,35 @@ git commit -m "Describe your change here"
The `.gitignore` file excludes `node_modules` and common system files from commits.
## Deploy for the whole company
Use this when the tool needs to be reachable by anyone on the internal network or VPN, not just on one person's machine. This uses a company-owned Anthropic API key shared by everyone who reaches the tool, so it adds a login gate and a request cap that a solo local setup does not need.
1. Get an Anthropic API key billed to a company account, not a personal one. IT or finance should provision this, since it is billed like any other company vendor cost.
2. Pick a shared username and password for this tool. This is a simple login gate, not a full identity system. It exists so the tool is not reachable by anyone who is merely on the same network segment, but treat the VPN and internal network as the primary control, not this login.
3. On the host or container platform, set these values as environment variables, or in a `.env` file next to `docker-compose.yml`:
```
ANTHROPIC_API_KEY=<company key>
APP_USERNAME=<shared username>
APP_PASSWORD=<shared password>
```
4. Build and run the container:
```
docker compose up -d --build
```
5. To run this alongside the existing Work Package Suite container instead of on its own, copy the `sde-meeting-toolkit` service block from `docker-compose.yml` into that stack's compose file, and apply the same environment variables there.
6. Confirm the login prompt appears when you open the tool's URL from another machine on the network.
### What the login gate does and does not do
- It requires a username and password before any page or API call on this tool succeeds.
- It does not encrypt traffic on its own. Run this behind the same network and VPN protections used for the Work Package Suite, and add TLS at the reverse proxy or load balancer if one is already in place for that stack.
- It does not track who made which AI draft request. Every user shares one login and one API key. If per-person attribution matters later, that needs a real identity integration, which is a larger change than this tool currently supports.
### Rate limit
`RATE_LIMIT_MAX` and `RATE_LIMIT_WINDOW_MS` cap AI draft requests per source IP address, to prevent a leaked link or a stuck script from running up cost on the shared key. Defaults: 20 requests per 5 minutes. Raise these in `.env` if real usage hits the limit; the tool returns a clear rate-limit error rather than failing silently.
## Relation to the Work Package Suite
These tools support meetings about the Work Package Suite project. The tools do not call the Work Package Suite application. The tools do not read or write Work Package Suite data. Treat this toolkit and the Work Package Suite codebase as separate projects.

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
# Standalone compose file for this tool.
#
# To run it alongside the Work Package Suite container instead, copy the
# "sde-meeting-toolkit" service block below into that stack's compose file.
# Put it on the same network as the other services only if you want
# hostname-based access between containers; this tool does not call the
# Work Package Suite API and does not need that network to function.
services:
sde-meeting-toolkit:
build: .
container_name: sde-meeting-toolkit
restart: unless-stopped
ports:
- "5173:5173"
env_file:
- .env

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