diff --git a/docker-compose.yml b/docker-compose.yml index a7ca969..4c976ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,8 @@ services: - TRIPS_BACKEND_URL=http://trips-service:8087 - FITNESS_BACKEND_URL=http://fitness-service:8095 - INVENTORY_BACKEND_URL=http://inventory-service:3000 + - INVENTORY_SERVICE_API_KEY=${INVENTORY_SERVICE_API_KEY} + - BUDGET_SERVICE_API_KEY=${BUDGET_SERVICE_API_KEY} - MINIFLUX_URL=${MINIFLUX_URL:-http://miniflux:8080} - MINIFLUX_API_KEY=${MINIFLUX_API_KEY} - TRIPS_API_TOKEN=${TRIPS_API_TOKEN} @@ -114,6 +116,7 @@ services: - PUBLIC_APP_URL=${PLATFORM_ORIGIN}/inventory - IMMICH_URL=${IMMICH_URL} - IMMICH_API_KEY=${IMMICH_API_KEY} + - SERVICE_API_KEY=${INVENTORY_SERVICE_API_KEY} - TZ=${TZ:-America/Chicago} networks: - default @@ -130,6 +133,7 @@ services: - ACTUAL_SERVER_URL=http://actualbudget:5006 - ACTUAL_PASSWORD=${ACTUAL_PASSWORD} - ACTUAL_SYNC_ID=${BUDGET_SYNC_ID} + - SERVICE_API_KEY=${BUDGET_SERVICE_API_KEY} - TZ=${TZ:-America/Chicago} networks: - default diff --git a/gateway/config.py b/gateway/config.py index fc27af6..d59a3be 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -23,6 +23,10 @@ SHELFMARK_URL = os.environ.get("SHELFMARK_URL", "http://shelfmark:8084") SPOTIZERR_URL = os.environ.get("SPOTIZERR_URL", "http://spotizerr-app:7171") BUDGET_URL = os.environ.get("BUDGET_BACKEND_URL", "http://localhost:3001") +# ── Service API keys (for internal service auth) ── +INVENTORY_SERVICE_API_KEY = os.environ.get("INVENTORY_SERVICE_API_KEY", "") +BUDGET_SERVICE_API_KEY = os.environ.get("BUDGET_SERVICE_API_KEY", "") + # ── Booklore (book library manager) ── BOOKLORE_URL = os.environ.get("BOOKLORE_URL", "http://booklore:6060") BOOKLORE_USER = os.environ.get("BOOKLORE_USER", "") diff --git a/gateway/dashboard.py b/gateway/dashboard.py index be455e3..787d195 100644 --- a/gateway/dashboard.py +++ b/gateway/dashboard.py @@ -5,7 +5,7 @@ Platform Gateway — Dashboard, apps, user profile, connections, pinning handler import json from datetime import datetime -from config import TRIPS_API_TOKEN, MINIFLUX_API_KEY +from config import TRIPS_API_TOKEN, MINIFLUX_API_KEY, INVENTORY_SERVICE_API_KEY, BUDGET_SERVICE_API_KEY from database import get_db from sessions import get_service_token, set_service_token, delete_service_token import proxy as _proxy_module @@ -212,12 +212,14 @@ def handle_dashboard(handler, user): def fetch_inventory(app): target = app["proxy_target"] - s1, _, b1 = proxy_request(f"{target}/needs-review-count", "GET", {}, timeout=10) + hdrs = {"X-API-Key": INVENTORY_SERVICE_API_KEY} if INVENTORY_SERVICE_API_KEY else {} + s1, _, b1 = proxy_request(f"{target}/needs-review-count", "GET", hdrs, timeout=10) return json.loads(b1) if s1 == 200 else None def fetch_budget(app): try: - s, _, b = proxy_request(f"{app['proxy_target']}/summary", "GET", {}, timeout=8) + hdrs = {"X-API-Key": BUDGET_SERVICE_API_KEY} if BUDGET_SERVICE_API_KEY else {} + s, _, b = proxy_request(f"{app['proxy_target']}/summary", "GET", hdrs, timeout=8) if s == 200: data = json.loads(b) return {"count": data.get("transactionCount", 0), "totalBalance": data.get("totalBalanceDollars", 0), "spending": data.get("spendingDollars", 0), "income": data.get("incomeDollars", 0), "topCategories": data.get("topCategories", [])[:5], "month": data.get("month", "")} diff --git a/gateway/server.py b/gateway/server.py index d0691e6..b123e1e 100644 --- a/gateway/server.py +++ b/gateway/server.py @@ -283,16 +283,21 @@ class GatewayHandler(ResponseMixin, BaseHTTPRequestHandler): self._send_json({"error": "Unknown service"}, 404) return - from config import MINIFLUX_API_KEY + from config import MINIFLUX_API_KEY, INVENTORY_SERVICE_API_KEY, BUDGET_SERVICE_API_KEY headers = {} ct = self.headers.get("Content-Type") if ct: headers["Content-Type"] = ct + # Inject service-level auth if service_id == "reader" and MINIFLUX_API_KEY: headers["X-Auth-Token"] = MINIFLUX_API_KEY elif service_id == "trips" and TRIPS_API_TOKEN: headers["Authorization"] = f"Bearer {TRIPS_API_TOKEN}" + elif service_id == "inventory" and INVENTORY_SERVICE_API_KEY: + headers["X-API-Key"] = INVENTORY_SERVICE_API_KEY + elif service_id == "budget" and BUDGET_SERVICE_API_KEY: + headers["X-API-Key"] = BUDGET_SERVICE_API_KEY elif user: svc_token = get_service_token(user["id"], service_id) if svc_token: diff --git a/services/budget/server.js b/services/budget/server.js index 3e2163a..6d5c0cc 100644 --- a/services/budget/server.js +++ b/services/budget/server.js @@ -11,6 +11,18 @@ const app = express(); app.use(cors()); app.use(express.json()); +// API key auth middleware — require X-API-Key header on all routes +const SERVICE_API_KEY = process.env.SERVICE_API_KEY || ''; +if (SERVICE_API_KEY) { + app.use((req, res, next) => { + const key = req.headers['x-api-key'] || req.query.api_key; + if (key !== SERVICE_API_KEY) { + return res.status(401).json({ error: 'Unauthorized: invalid API key' }); + } + next(); + }); +} + // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- diff --git a/services/inventory/server.js b/services/inventory/server.js index 9147965..4dfeb17 100755 --- a/services/inventory/server.js +++ b/services/inventory/server.js @@ -27,6 +27,18 @@ app.use(express.json()); // Allow form-encoded payloads from NocoDB webhook buttons app.use(express.urlencoded({ extended: true })); +// API key auth middleware — require X-API-Key header on all routes +const SERVICE_API_KEY = process.env.SERVICE_API_KEY || ''; +if (SERVICE_API_KEY) { + app.use((req, res, next) => { + const key = req.headers['x-api-key'] || req.query.api_key; + if (key !== SERVICE_API_KEY) { + return res.status(401).json({ error: 'Unauthorized: invalid API key' }); + } + next(); + }); +} + const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }