From b2107f9f1a14cf23735dcc4e7892751bf4674f2f Mon Sep 17 00:00:00 2001 From: B SIMAK AHMED <1NZ24CS030@newhorizonindia.edu> Date: Sat, 26 Sep 2026 19:10:33 +0530 Subject: [PATCH 1/3] feat: add privacy protection components --- src/components/PrivacyBlur.tsx | 16 +++++++++++++++ src/components/PrivacyCopyrightNotice.tsx | 15 ++++++++++++++ src/components/ProtectedImage.tsx | 25 +++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/components/PrivacyBlur.tsx create mode 100644 src/components/PrivacyCopyrightNotice.tsx create mode 100644 src/components/ProtectedImage.tsx 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()} + > + {alt} + + + ); +} From a2b30787e47b96f1e96c44c3021a6e711dd87cbc Mon Sep 17 00:00:00 2001 From: B SIMAK AHMED <1NZ24CS030@newhorizonindia.edu> Date: Sat, 26 Sep 2026 19:19:16 +0530 Subject: [PATCH 2/3] feat: integrate privacy protections --- src/components/SiteLayout.tsx | 2 ++ src/routes/__root.tsx | 8 ++---- src/routes/_authenticated/route.tsx | 42 +++++------------------------ vercel.json | 37 +++++++++---------------- 4 files changed, 23 insertions(+), 66 deletions(-) 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 (
{children}
+
); 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" } ] } ] From caf4ad381f8bd35ff4b627731e380c43f14c6a3f Mon Sep 17 00:00:00 2001 From: B SIMAK AHMED <1NZ24CS030@newhorizonindia.edu> Date: Sun, 27 Sep 2026 13:32:12 +0530 Subject: [PATCH 3/3] fix: handle Windows line endings in admin build script --- scripts/ensure-admin-build.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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
{label}
{value}
; }`; @@ -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");