mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add privacy policy page and fix CSP blocking API docs
Add a privacy policy page accessible at /privacy (public, no auth required). Relax Content-Security-Policy for /api/docs route to allow Scalar's inline script initialization, fixing blank docs page in production.
This commit is contained in:
@@ -53,10 +53,10 @@ app.addHook("onSend", async (_request, reply) => {
|
|||||||
reply.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
reply.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
reply.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
reply.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
reply.header(
|
const csp = _request.url.startsWith("/api/docs")
|
||||||
"Content-Security-Policy",
|
? "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self'; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'"
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self'; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
|
: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self'; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
|
||||||
);
|
reply.header("Content-Security-Policy", csp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { FilesPage } from "./pages/files-page";
|
|||||||
import { FullscreenGridPage } from "./pages/fullscreen-grid-page";
|
import { FullscreenGridPage } from "./pages/fullscreen-grid-page";
|
||||||
import { HomePage } from "./pages/home-page";
|
import { HomePage } from "./pages/home-page";
|
||||||
import { LoginPage } from "./pages/login-page";
|
import { LoginPage } from "./pages/login-page";
|
||||||
|
import { PrivacyPolicyPage } from "./pages/privacy-policy-page";
|
||||||
import { ToolPage } from "./pages/tool-page";
|
import { ToolPage } from "./pages/tool-page";
|
||||||
|
|
||||||
class ErrorBoundary extends Component<
|
class ErrorBoundary extends Component<
|
||||||
@@ -59,7 +60,11 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Don't guard the login or change-password pages
|
// Don't guard the login or change-password pages
|
||||||
if (location.pathname === "/login" || location.pathname === "/change-password") {
|
if (
|
||||||
|
location.pathname === "/login" ||
|
||||||
|
location.pathname === "/change-password" ||
|
||||||
|
location.pathname === "/privacy"
|
||||||
|
) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +103,7 @@ export function App() {
|
|||||||
<Route path="/automate" element={<AutomatePage />} />
|
<Route path="/automate" element={<AutomatePage />} />
|
||||||
<Route path="/files" element={<FilesPage />} />
|
<Route path="/files" element={<FilesPage />} />
|
||||||
<Route path="/fullscreen" element={<FullscreenGridPage />} />
|
<Route path="/fullscreen" element={<FullscreenGridPage />} />
|
||||||
|
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
||||||
<Route path="/:toolId" element={<ToolPage />} />
|
<Route path="/:toolId" element={<ToolPage />} />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
|||||||
</div>
|
</div>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
|
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
|
||||||
|
<Link to="/privacy" className="hover:text-foreground transition-colors">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export function PrivacyPolicyPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
|
<div className="max-w-2xl mx-auto px-6 py-12">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mb-8"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Back to app
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<h1 className="text-3xl font-bold mb-2">Privacy Policy</h1>
|
||||||
|
<p className="text-sm text-muted-foreground mb-8">Last updated: March 29, 2026</p>
|
||||||
|
|
||||||
|
<div className="space-y-6 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">Overview</h2>
|
||||||
|
<p>
|
||||||
|
Stirling Image is a self-hosted, open-source image processing application. Your
|
||||||
|
instance is operated and controlled entirely by whoever deployed it. This policy
|
||||||
|
describes how the software itself handles your data.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">Local Processing</h2>
|
||||||
|
<p>
|
||||||
|
All image processing happens entirely on the server where Stirling Image is deployed.
|
||||||
|
Your images are never sent to external services or third-party APIs. When you upload
|
||||||
|
an image for processing, it is handled in memory or in temporary storage on the host
|
||||||
|
machine and is not retained after the operation completes.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">No Tracking or Analytics</h2>
|
||||||
|
<p>
|
||||||
|
Stirling Image does not include any telemetry, analytics, or tracking. No data is
|
||||||
|
collected about your usage patterns, and no information is sent to Stirling Image
|
||||||
|
developers or any third party. There are no cookies used for tracking purposes.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">Data Storage</h2>
|
||||||
|
<p>
|
||||||
|
If authentication is enabled, the application stores user accounts (usernames and
|
||||||
|
hashed passwords) in a local SQLite database on the host machine. If you use the Files
|
||||||
|
feature, uploaded files are stored on the server's filesystem. All stored data remains
|
||||||
|
entirely under the control of the instance operator.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">No Third-Party Services</h2>
|
||||||
|
<p>
|
||||||
|
Stirling Image does not integrate with or send data to any external services.
|
||||||
|
AI-powered features (background removal, upscaling, OCR) run locally using bundled
|
||||||
|
models. No cloud APIs are involved.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">Open Source</h2>
|
||||||
|
<p>
|
||||||
|
Stirling Image is fully open source. You can audit the source code to verify these
|
||||||
|
claims at any time. Transparency is a core principle of this project.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">Your Control</h2>
|
||||||
|
<p>
|
||||||
|
Because Stirling Image is self-hosted, the instance operator has full control over all
|
||||||
|
data. You can delete your data at any time by removing files from the server or
|
||||||
|
deleting the database. No data exists outside of your infrastructure.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user