diff --git a/scripts/ensure-admin-build.mjs b/scripts/ensure-admin-build.mjs
index 712b847..3e4d099 100644
--- a/scripts/ensure-admin-build.mjs
+++ b/scripts/ensure-admin-build.mjs
@@ -5,6 +5,8 @@ import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const file = path.join(root, "src", "routes", "_authenticated", "admin.tsx");
let source = fs.readFileSync(file, "utf8");
+const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
+source = source.replace(/\r\n/g, "\n");
// Keep the existing production build guard for the historical duplicate Metric.
const duplicate = `\nfunction Metric({ label, value }: { label: string; value: number }) { return
; }`;
@@ -50,5 +52,5 @@ if (!source.includes(devicesRender)) {
source = source.replace(anchor, `${anchor}\n ${devicesRender}`);
}
-fs.writeFileSync(file, source, "utf8");
+fs.writeFileSync(file, source.replace(/\n/g, lineEnding), "utf8");
console.log("Admin Devices integration: ready");
diff --git a/src/components/PrivacyBlur.tsx b/src/components/PrivacyBlur.tsx
new file mode 100644
index 0000000..aeba6c3
--- /dev/null
+++ b/src/components/PrivacyBlur.tsx
@@ -0,0 +1,16 @@
+import { useEffect, useState } from "react";
+
+export function PrivacyBlur({ children }: { children: React.ReactNode }) {
+ const [isHidden, setIsHidden] = useState(false);
+
+ useEffect(() => {
+ const handleVisibilityChange = () => {
+ setIsHidden(document.visibilityState === "hidden");
+ };
+
+ document.addEventListener("visibilitychange", handleVisibilityChange);
+ return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
+ }, []);
+
+ return {children}
;
+}
diff --git a/src/components/PrivacyCopyrightNotice.tsx b/src/components/PrivacyCopyrightNotice.tsx
new file mode 100644
index 0000000..ff4ff92
--- /dev/null
+++ b/src/components/PrivacyCopyrightNotice.tsx
@@ -0,0 +1,15 @@
+export function PrivacyCopyrightNotice() {
+ return (
+
+ );
+}
diff --git a/src/components/ProtectedImage.tsx b/src/components/ProtectedImage.tsx
new file mode 100644
index 0000000..1a38531
--- /dev/null
+++ b/src/components/ProtectedImage.tsx
@@ -0,0 +1,25 @@
+import type { ImgHTMLAttributes } from "react";
+
+type ProtectedImageProps = Omit, "draggable"> & {
+ watermark?: string;
+};
+
+export function ProtectedImage({ watermark = "© CodeForge", className, alt = "", ...props }: ProtectedImageProps) {
+ return (
+ event.preventDefault()}
+ onDragStart={(event) => event.preventDefault()}
+ >
+
+
+ {watermark}
+
+
+ );
+}
diff --git a/src/components/SiteLayout.tsx b/src/components/SiteLayout.tsx
index e7c2e40..75f44f4 100644
--- a/src/components/SiteLayout.tsx
+++ b/src/components/SiteLayout.tsx
@@ -1,12 +1,14 @@
import type { ReactNode } from "react";
import { Navbar } from "./Navbar";
import { Footer } from "./Footer";
+import { PrivacyCopyrightNotice } from "./PrivacyCopyrightNotice";
export function SiteLayout({ children }: { children: ReactNode }) {
return (
);
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 7dd1f5c..9df327d 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -124,11 +124,7 @@ function RootShell({ children }: { children: ReactNode }) {
- event.preventDefault()}
- onCopy={(event) => event.preventDefault()}
- onCut={(event) => event.preventDefault()}
- >
+
{children}
@@ -149,4 +145,4 @@ function RootComponent() {
);
-}
\ No newline at end of file
+}
diff --git a/src/routes/_authenticated/route.tsx b/src/routes/_authenticated/route.tsx
index 5357f80..0efa84b 100644
--- a/src/routes/_authenticated/route.tsx
+++ b/src/routes/_authenticated/route.tsx
@@ -1,20 +1,3 @@
-/**
- * _authenticated layout — guards /dashboard and /admin
- *
- * Logic:
- * 1. No session → /auth
- * 2. No verified TOTP factor → /mfa-setup
- * 3. Has verified TOTP factor BUT session is only AAL1 → /mfa-verify
- * 4. AAL2 session → allow through
- *
- * MFA is mandatory for the admin account.
- *
- * Device-session monitoring:
- * - Registers the current admin browser/device.
- * - Periodically asks Supabase Auth whether the session still exists.
- * - A session revoked from Admin → Devices is therefore detected and the
- * remote browser is signed out through the normal local logout flow.
- */
import { createFileRoute, Outlet, redirect, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { supabase } from "@/integrations/supabase/client";
@@ -22,31 +5,21 @@ import {
registerCurrentAdminSession,
touchCurrentAdminSession,
} from "@/lib/admin-sessions";
+import { PrivacyBlur } from "@/components/PrivacyBlur";
export const Route = createFileRoute("/_authenticated")({
ssr: false,
+ head: () => ({ meta: [{ name: "robots", content: "noindex, nofollow" }] }),
beforeLoad: async () => {
- // 1. Require a valid session. getUser() performs a network check against
- // Supabase Auth, so revoked remote sessions are not treated as valid.
const { data: userData, error } = await supabase.auth.getUser();
if (error || !userData.user) throw redirect({ to: "/auth" });
- // 2. Check if this user has MFA enrolled
const { data: factors } = await supabase.auth.mfa.listFactors();
const hasVerifiedFactor = (factors?.totp ?? []).length > 0;
+ if (!hasVerifiedFactor) throw redirect({ to: "/mfa-setup" });
- if (!hasVerifiedFactor) {
- throw redirect({ to: "/mfa-setup" });
- }
-
- {
- // User has MFA — enforce AAL2 for this session
- const { data: aalData } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
- if (aalData?.currentLevel !== "aal2") {
- // Session is password-only — redirect to MFA verification
- throw redirect({ to: "/mfa-verify" });
- }
- }
+ const { data: aalData } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
+ if (aalData?.currentLevel !== "aal2") throw redirect({ to: "/mfa-verify" });
return { user: userData.user };
},
@@ -69,20 +42,17 @@ function AuthenticatedLayout() {
return;
}
- // Only admin accounts are registered in the Devices panel. Failure to
- // register must not block the existing authenticated route.
await registerCurrentAdminSession();
await touchCurrentAdminSession();
};
void monitorSession();
const timer = window.setInterval(() => void monitorSession(), 15_000);
-
return () => {
cancelled = true;
window.clearInterval(timer);
};
}, [navigate]);
- return ;
+ return ;
}
diff --git a/vercel.json b/vercel.json
index f05bdac..b1d0b33 100644
--- a/vercel.json
+++ b/vercel.json
@@ -1,33 +1,22 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"headers": [
+ { "source": "/auth", "headers": [{ "key": "Cache-Control", "value": "no-store" }] },
+ { "source": "/forgot-password", "headers": [{ "key": "Cache-Control", "value": "no-store" }] },
+ { "source": "/reset-password", "headers": [{ "key": "Cache-Control", "value": "no-store" }] },
+ { "source": "/mfa-setup", "headers": [{ "key": "Cache-Control", "value": "no-store" }] },
+ { "source": "/mfa-verify", "headers": [{ "key": "Cache-Control", "value": "no-store" }] },
+ { "source": "/dashboard/:path*", "headers": [{ "key": "Cache-Control", "value": "no-store" }] },
+ { "source": "/admin/:path*", "headers": [{ "key": "Cache-Control", "value": "no-store" }] },
{
"source": "/(.*)",
"headers": [
- {
- "key": "Content-Security-Policy",
- "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https://*.supabase.co; connect-src 'self' https://*.supabase.co https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; worker-src 'self' blob:; manifest-src 'self'; media-src 'self'; upgrade-insecure-requests"
- },
- {
- "key": "X-Content-Type-Options",
- "value": "nosniff"
- },
- {
- "key": "X-Frame-Options",
- "value": "DENY"
- },
- {
- "key": "Referrer-Policy",
- "value": "strict-origin-when-cross-origin"
- },
- {
- "key": "Permissions-Policy",
- "value": "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
- },
- {
- "key": "Strict-Transport-Security",
- "value": "max-age=31536000; includeSubDomains"
- }
+ { "key": "Content-Security-Policy", "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https://*.supabase.co; connect-src 'self' https://*.supabase.co https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; worker-src 'self' blob:; manifest-src 'self'; media-src 'self'; upgrade-insecure-requests" },
+ { "key": "X-Content-Type-Options", "value": "nosniff" },
+ { "key": "X-Frame-Options", "value": "DENY" },
+ { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
+ { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=(), payment=(), usb=()" },
+ { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }
]
}
]