mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: Docker hardening, security, and deployment readiness for V1 (#82)
Phase 1 — Docker Artifact Optimization: - Replace broad `COPY . .` with targeted frontend source copies (API/Python changes no longer bust the frontend build cache) - Replace build-essential with gcc/g++ (leaner runtime) - Fix LOG_LEVEL=debug → info for production - Harden .dockerignore (exclude worktrees, IDE, CI, test artifacts) Phase 2 — State & Persistence: - Add PUID/PGID support in entrypoint.sh for bind mount compatibility - Guard against PUID=0/PGID=0 to prevent accidental root execution - Evict conflicting system users (e.g. node:1000) before UID remap Phase 3 — Security: - Always register @fastify/rate-limit so login brute-force protection works even when global rate limit is disabled (RATE_LIMIT_PER_MIN=0) - Add trustProxy support (TRUST_PROXY env var, default true) so rate limiting and audit logs use real client IPs behind reverse proxies - Strip stack traces from 500 error responses in production - Fix FSTDEP022 deprecation: maxParamLength → routerOptions - Add multi-file guard on single-file tool endpoint with clear error message pointing to the /batch endpoint Phase 4 — Graceful Degradation: - Add consolidated hardware detection startup banner (GPU, rate limit, upload limit, proxy status) - Add ConnectionMonitor component with health polling and reconnecting overlay that auto-dismisses when the server comes back Phase 5 — Deployment Docs: - Rewrite deployment.md with copy-paste CPU and GPU compose templates - Add hardware requirements table (minimum, recommended, heavy workloads) - Add PUID/PGID bind mount documentation - Add complete env var reference table - Add reverse proxy guides for Nginx, Nginx Proxy Manager, Traefik, and Cloudflare Tunnels
This commit is contained in:
+45
-103
@@ -1,50 +1,40 @@
|
||||
import { Component, type ErrorInfo, type ReactNode, Suspense } from "react";
|
||||
import { Component, type ErrorInfo, lazy, type ReactNode, Suspense } from "react";
|
||||
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { Toaster } from "sonner";
|
||||
import { ConnectionBanner } from "./components/common/connection-banner";
|
||||
import { ConnectionMonitor } from "./components/common/connection-monitor";
|
||||
import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider";
|
||||
import { useAuth } from "./hooks/use-auth";
|
||||
import { useConnectionMonitor } from "./hooks/use-connection-monitor";
|
||||
import { isChunkError, lazyWithRetry } from "./lib/lazy-with-retry";
|
||||
|
||||
// Lazy-load all pages with automatic retry so chunk failures from
|
||||
// deployments are recovered transparently instead of white-screening.
|
||||
const AutomatePage = lazyWithRetry(() =>
|
||||
// Lazy-load all pages so each page's JS (and its icons/deps) is only
|
||||
// downloaded when the user navigates there, shrinking the main bundle.
|
||||
const AutomatePage = lazy(() =>
|
||||
import("./pages/automate-page").then((m) => ({ default: m.AutomatePage })),
|
||||
);
|
||||
const ChangePasswordPage = lazyWithRetry(() =>
|
||||
const ChangePasswordPage = lazy(() =>
|
||||
import("./pages/change-password-page").then((m) => ({ default: m.ChangePasswordPage })),
|
||||
);
|
||||
const FilesPage = lazyWithRetry(() =>
|
||||
import("./pages/files-page").then((m) => ({ default: m.FilesPage })),
|
||||
);
|
||||
const FullscreenGridPage = lazyWithRetry(() =>
|
||||
const FilesPage = lazy(() => import("./pages/files-page").then((m) => ({ default: m.FilesPage })));
|
||||
const FullscreenGridPage = lazy(() =>
|
||||
import("./pages/fullscreen-grid-page").then((m) => ({ default: m.FullscreenGridPage })),
|
||||
);
|
||||
const HomePage = lazyWithRetry(() =>
|
||||
import("./pages/home-page").then((m) => ({ default: m.HomePage })),
|
||||
);
|
||||
const LoginPage = lazyWithRetry(() =>
|
||||
import("./pages/login-page").then((m) => ({ default: m.LoginPage })),
|
||||
);
|
||||
const PrivacyPolicyPage = lazyWithRetry(() =>
|
||||
const HomePage = lazy(() => import("./pages/home-page").then((m) => ({ default: m.HomePage })));
|
||||
const LoginPage = lazy(() => import("./pages/login-page").then((m) => ({ default: m.LoginPage })));
|
||||
const PrivacyPolicyPage = lazy(() =>
|
||||
import("./pages/privacy-policy-page").then((m) => ({ default: m.PrivacyPolicyPage })),
|
||||
);
|
||||
const ToolPage = lazyWithRetry(() =>
|
||||
import("./pages/tool-page").then((m) => ({ default: m.ToolPage })),
|
||||
);
|
||||
const ToolPage = lazy(() => import("./pages/tool-page").then((m) => ({ default: m.ToolPage })));
|
||||
|
||||
class ErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ hasError: boolean; error: Error | null; isChunkError: boolean }
|
||||
{ hasError: boolean; error: Error | null }
|
||||
> {
|
||||
constructor(props: { children: ReactNode }) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, isChunkError: false };
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error, isChunkError: isChunkError(error) };
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
@@ -53,43 +43,6 @@ class ErrorBoundary extends Component<
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.state.isChunkError) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-background text-foreground">
|
||||
<div className="text-center space-y-4 max-w-md px-6">
|
||||
<div className="mx-auto h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<svg
|
||||
className="h-6 w-6 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
role="img"
|
||||
aria-label="Refresh icon"
|
||||
>
|
||||
<title>Refresh</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold">Update Available</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A new version of ashim has been deployed.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-background text-foreground">
|
||||
<div className="text-center space-y-4 max-w-md px-6">
|
||||
@@ -100,7 +53,7 @@ class ErrorBoundary extends Component<
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
this.setState({ hasError: false, error: null, isChunkError: false });
|
||||
this.setState({ hasError: false, error: null });
|
||||
window.location.href = "/";
|
||||
}}
|
||||
className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium"
|
||||
@@ -160,48 +113,37 @@ function PageLoader() {
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectionMonitor() {
|
||||
useConnectionMonitor();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<ConnectionMonitor />
|
||||
<ConnectionBanner />
|
||||
<ErrorBoundary>
|
||||
<Toaster position="bottom-right" />
|
||||
<BrowserRouter>
|
||||
<KeyboardShortcutProvider>
|
||||
<AuthGuard>
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/change-password" element={<ChangePasswordPage />} />
|
||||
<Route path="/automate" element={<AutomatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/fullscreen" element={<FullscreenGridPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
||||
{/* Redirects: old color tools consolidated into adjust-colors */}
|
||||
<Route
|
||||
path="/brightness-contrast"
|
||||
element={<Navigate to="/adjust-colors" replace />}
|
||||
/>
|
||||
<Route path="/saturation" element={<Navigate to="/adjust-colors" replace />} />
|
||||
<Route
|
||||
path="/color-channels"
|
||||
element={<Navigate to="/adjust-colors" replace />}
|
||||
/>
|
||||
<Route path="/color-effects" element={<Navigate to="/adjust-colors" replace />} />
|
||||
<Route path="/:toolId" element={<ToolPage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AuthGuard>
|
||||
</KeyboardShortcutProvider>
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
<Toaster position="bottom-right" />
|
||||
<BrowserRouter>
|
||||
<KeyboardShortcutProvider>
|
||||
<AuthGuard>
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/change-password" element={<ChangePasswordPage />} />
|
||||
<Route path="/automate" element={<AutomatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/fullscreen" element={<FullscreenGridPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
||||
{/* Redirects: old color tools consolidated into adjust-colors */}
|
||||
<Route
|
||||
path="/brightness-contrast"
|
||||
element={<Navigate to="/adjust-colors" replace />}
|
||||
/>
|
||||
<Route path="/saturation" element={<Navigate to="/adjust-colors" replace />} />
|
||||
<Route path="/color-channels" element={<Navigate to="/adjust-colors" replace />} />
|
||||
<Route path="/color-effects" element={<Navigate to="/adjust-colors" replace />} />
|
||||
<Route path="/:toolId" element={<ToolPage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AuthGuard>
|
||||
</KeyboardShortcutProvider>
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user