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.

+ +