From a03ae5b62c51f08298dc0ac908688365a7ce55e2 Mon Sep 17 00:00:00 2001 From: isnowglobal-admin Date: Sat, 13 Jun 2026 14:21:27 -0400 Subject: [PATCH] Security hardening: rate limit whitelist for home subnet, relaxed dev limits, ORATS fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .env to .gitignore (already present) - Whitelist 192.168.x.x, 10.x.x.x, 127.x in rate limiter - Relax rate limits: 500/15min general, 50/15min auth (was 100/10) - ORATS API v2 integration: datav2 base URL, ?token= auth, flat response parsing - GEX metrics: DTE ≤ 7 filter, 80-120% strike range, 500 OI threshold - Save button: real-time singleton update, cache invalidation - Server binds to 0.0.0.0 for network access - Added Docker support, security middleware, auth routes --- .dockerignore | 11 + Dockerfile | 32 +++ client/src/pages/settings.tsx | 59 +++++- docker-compose.yml | 54 +++++ package-lock.json | 94 ++++++++- package.json | 4 + server/authRoutes.ts | 220 ++++++++++++++------ server/db.ts | 2 +- server/index.ts | 3 +- server/middleware.ts | 100 ++++++++++ server/oratsClient.ts | 364 ++++++++++++++++++++-------------- server/routes.ts | 102 +++++++--- server/security.ts | 250 +++++++++++++++++++++++ server/static.ts | 5 +- start.sh | 4 + 15 files changed, 1056 insertions(+), 248 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 server/middleware.ts create mode 100644 server/security.ts create mode 100755 start.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..94770d3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +dist +.env +.env.local +data.db +*.log +.git +.gitignore +.vscode +.idea +docs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ae40ff5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM node:20-alpine + +WORKDIR /app + +RUN apk add --no-cache curl tini + +# Copy everything +COPY . . + +# Install all deps +RUN npm ci + +# Build the app (creates dist/public/ for static serving) +RUN npm run build + +# Create non-root user for security +RUN addgroup -S appgroup && adduser -S appuser -G appgroup && chown -R appuser:appgroup /app + +USER appuser + +ENV NODE_ENV=production +ENV PORT=3000 + +EXPOSE 3000 + +ENTRYPOINT ["tini", "--"] + +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:3000/api/orats/status || exit 1 + +# Run with tsx so the built server + dist/public/ are available +CMD ["npx", "tsx", "server/index.ts"] diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index 91e5766..f7b074d 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { CheckCircle2, AlertTriangle, ExternalLink } from "lucide-react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { CheckCircle2, AlertTriangle, ExternalLink, Loader2 } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -15,11 +15,41 @@ interface OratsStatus { } export default function SettingsPage() { + const queryClient = useQueryClient(); // Form state is React-only — values never touch localStorage/sessionStorage/cookies. const [apiKey, setApiKey] = useState(""); const [baseUrl, setBaseUrl] = useState("https://api.orats.io/datav2"); const { data: status } = useQuery({ queryKey: ["/api/orats/status"] }); + const saveMutation = useMutation({ + mutationFn: async (key: string) => { + const res = await fetch("/api/orats/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ apiKey: key, baseUrl }), + }); + if (!res.ok) throw new Error("Failed to save API key"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/orats/status"] }); + // Also invalidate all market data to force fresh ORATS fetch + queryClient.invalidateQueries({ queryKey: ["/api/market"] }); + }, + }); + + const clearMutation = useMutation({ + mutationFn: async () => { + const res = await fetch("/api/orats/config", { method: "DELETE" }); + if (!res.ok) throw new Error("Failed to clear API key"); + return res.json(); + }, + onSuccess: () => { + setApiKey(""); + queryClient.invalidateQueries({ queryKey: ["/api/orats/status"] }); + }, + }); + return (
@@ -52,7 +82,7 @@ export default function SettingsPage() { {status.configured ? "Endpoints will attempt live ORATS calls. Verify network egress and rate limits." - : "Set the ORATS_API_KEY environment variable on the server to switch from mock data to live ORATS. The form below is for session-only inspection — values are never persisted to disk or browser storage."} + : "Set the ORATS_API_KEY environment variable on the server to switch from mock data to live ORATS."} )} @@ -75,7 +105,7 @@ export default function SettingsPage() {

- Kept in React state only. To use in production, set ORATS_API_KEY on the server environment. + Your ORATS API key — saved to your session when you click Save.

+ +