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=()");
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
reply.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||
reply.header(
|
||||
"Content-Security-Policy",
|
||||
"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'",
|
||||
);
|
||||
const csp = _request.url.startsWith("/api/docs")
|
||||
? "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'";
|
||||
reply.header("Content-Security-Policy", csp);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FilesPage } from "./pages/files-page";
|
||||
import { FullscreenGridPage } from "./pages/fullscreen-grid-page";
|
||||
import { HomePage } from "./pages/home-page";
|
||||
import { LoginPage } from "./pages/login-page";
|
||||
import { PrivacyPolicyPage } from "./pages/privacy-policy-page";
|
||||
import { ToolPage } from "./pages/tool-page";
|
||||
|
||||
class ErrorBoundary extends Component<
|
||||
@@ -59,7 +60,11 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const location = useLocation();
|
||||
|
||||
// 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}</>;
|
||||
}
|
||||
|
||||
@@ -98,6 +103,7 @@ export function App() {
|
||||
<Route path="/automate" element={<AutomatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/fullscreen" element={<FullscreenGridPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
||||
<Route path="/:toolId" element={<ToolPage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -124,7 +124,9 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="text-center text-xs text-muted-foreground py-2 border-t border-border">
|
||||
Privacy Policy
|
||||
<Link to="/privacy" className="hover:text-foreground transition-colors">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</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