diff --git a/components/frontend/next.config.js b/components/frontend/next.config.js index bbab259ea..9e8fe5203 100644 --- a/components/frontend/next.config.js +++ b/components/frontend/next.config.js @@ -1,6 +1,57 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone' + output: 'standalone', + + async headers() { + return [ + { + // Apply security headers to all routes + source: '/:path*', + headers: [ + // Content Security Policy + // - script-src: Allow 'self' scripts only (next-themes handles dark mode without inline scripts) + // - style-src: Allow 'self' and 'unsafe-inline' (required for Tailwind CSS) + // - connect-src: Allow 'self' and WebSocket connections (ws:/wss:) for backend communication + // - default-src: Restrict all other resources to same-origin only + { + key: 'Content-Security-Policy', + value: [ + "default-src 'self'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + "connect-src 'self' ws: wss:", + "img-src 'self' data: blob:", + "font-src 'self' data:", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + ].join('; '), + }, + // Prevent clickjacking attacks by disallowing iframe embedding + { + key: 'X-Frame-Options', + value: 'DENY', + }, + // Prevent MIME type sniffing + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + // Control referrer information sent with requests + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + // Restrict browser features and APIs + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()', + }, + ], + }, + ]; + }, } module.exports = nextConfig diff --git a/components/frontend/package.json b/components/frontend/package.json index f5931698c..0befba125 100644 --- a/components/frontend/package.json +++ b/components/frontend/package.json @@ -29,6 +29,7 @@ "highlight.js": "^11.11.1", "lucide-react": "^0.542.0", "next": "15.5.2", + "next-themes": "^0.4.4", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", diff --git a/components/frontend/src/app/layout.tsx b/components/frontend/src/app/layout.tsx index ed7e98006..5c90a7ff3 100644 --- a/components/frontend/src/app/layout.tsx +++ b/components/frontend/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from "next/font/google"; import "./globals.css"; import { Navigation } from "@/components/navigation"; import { QueryProvider } from "@/components/providers/query-provider"; +import { ThemeProvider } from "@/components/providers/theme-provider"; import { Toaster } from "@/components/ui/toaster"; import { env } from "@/lib/env"; @@ -27,11 +28,18 @@ export default function RootLayout({ - - -
{children}
- -
+ + + +
{children}
+ +
+
); diff --git a/components/frontend/src/components/navigation.tsx b/components/frontend/src/components/navigation.tsx index 7a6cff5aa..f08033ce3 100644 --- a/components/frontend/src/components/navigation.tsx +++ b/components/frontend/src/components/navigation.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { UserBubble } from "@/components/user-bubble"; +import ThemeToggle from "@/components/theme-toggle"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Plug, LogOut } from "lucide-react"; import { useVersion } from "@/services/queries/use-version"; @@ -44,7 +45,7 @@ export function Navigation({ feedbackUrl }: NavigationProps) {
{feedbackUrl && ( - )} + diff --git a/components/frontend/src/components/providers/theme-provider.tsx b/components/frontend/src/components/providers/theme-provider.tsx new file mode 100644 index 000000000..8cccd3753 --- /dev/null +++ b/components/frontend/src/components/providers/theme-provider.tsx @@ -0,0 +1,9 @@ +'use client'; + +import * as React from 'react'; +import { ThemeProvider as NextThemesProvider } from 'next-themes'; +import type { ThemeProviderProps } from 'next-themes'; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/components/frontend/src/components/theme-toggle.tsx b/components/frontend/src/components/theme-toggle.tsx new file mode 100644 index 000000000..516c90187 --- /dev/null +++ b/components/frontend/src/components/theme-toggle.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { Moon, Sun } from 'lucide-react'; +import { useTheme } from 'next-themes'; +import { Button } from '@/components/ui/button'; + +export default function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +}