diff --git a/.github/workflows/railway-deploy.yml b/.github/workflows/railway-deploy.yml index 4ba4ad0..b66329c 100644 --- a/.github/workflows/railway-deploy.yml +++ b/.github/workflows/railway-deploy.yml @@ -70,6 +70,8 @@ jobs: - name: Build Next.js application run: npm run build + env: + SKIP_ENV_VALIDATION: true - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/middleware.ts b/middleware.ts index 34cae11..dc103a6 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,10 +1,13 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; /** * ============================================================================ - * CLERK MIDDLEWARE - CENTRALIZED AUTHENTICATION + * CLERK MIDDLEWARE - CENTRALIZED AUTHENTICATION & SECURITY * ============================================================================ * This middleware protects routes and handles authentication across the app. + * Also adds security headers to all responses. * * Route Protection: * - Public routes: Landing page, sign-in, sign-up, public API endpoints @@ -14,6 +17,7 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; * 1. Public routes are accessible to everyone * 2. Protected routes redirect to sign-in if not authenticated * 3. API routes return 401 if not authenticated (handled in route handlers) + * 4. Security headers are added to all responses */ // Define public routes that don't require authentication @@ -22,13 +26,71 @@ const isPublicRoute = createRouteMatcher([ "/sign-in(.*)", // Sign in page and sub-routes "/sign-up(.*)", // Sign up page and sub-routes "/api/webhooks/(.*)", // Webhook endpoints (verified by webhook secret) + "/api/health", // Health check endpoint ]); -export default clerkMiddleware(async (auth, request) => { +/** + * Security headers to protect against common attacks + */ +const securityHeaders = { + // Prevent clickjacking + "X-Frame-Options": "DENY", + // Prevent MIME type sniffing + "X-Content-Type-Options": "nosniff", + // Control referrer information + "Referrer-Policy": "strict-origin-when-cross-origin", + // Restrict browser features + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", + // XSS protection (legacy, but still useful) + "X-XSS-Protection": "1; mode=block", +}; + +/** + * Content Security Policy - adjust as needed for your specific requirements + */ +const cspHeader = ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://clerk.com https://*.clerk.accounts.dev https://challenges.cloudflare.com https://prod.spline.design; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + img-src 'self' blob: data: https: http:; + font-src 'self' https://fonts.gstatic.com data:; + connect-src 'self' https://*.clerk.accounts.dev https://clerk.com https://api.clerk.com https://*.github.com https://api.github.com wss://*.clerk.accounts.dev https://prod.spline.design https://*.firebase.com https://*.firebaseio.com https://*.googleapis.com; + frame-src 'self' https://clerk.com https://*.clerk.accounts.dev https://challenges.cloudflare.com https://prod.spline.design; + worker-src 'self' blob:; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; +`.replace(/\s{2,}/g, ' ').trim(); + +export default clerkMiddleware(async (auth, request: NextRequest) => { // Protect all routes except public ones if (!isPublicRoute(request)) { await auth.protect(); } + + // Get response (will be created by next middleware/handler) + const response = NextResponse.next(); + + // Add security headers + for (const [key, value] of Object.entries(securityHeaders)) { + response.headers.set(key, value); + } + + // Add CSP header (only in production to avoid dev issues) + if (process.env.NODE_ENV === "production") { + response.headers.set("Content-Security-Policy", cspHeader); + } + + // Add HSTS header (only in production over HTTPS) + if (process.env.NODE_ENV === "production") { + response.headers.set( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains; preload" + ); + } + + return response; }); export const config = { @@ -39,3 +101,4 @@ export const config = { "/(api|trpc)(.*)", ], }; + diff --git a/next.config.ts b/next.config.ts index 2636543..091532c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,12 +15,28 @@ const nextConfig: NextConfig = { NEXT_PUBLIC_SEPOLIA_RPC_URL: process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL, NEXT_PUBLIC_EQUITY_TOKEN_ADDRESS: process.env.NEXT_PUBLIC_EQUITY_TOKEN_ADDRESS, }, - // Optimize image handling + // Optimize image handling with specific domains images: { remotePatterns: [ { protocol: 'https', - hostname: '**', + hostname: 'images.unsplash.com', + }, + { + protocol: 'https', + hostname: 'assets.aceternity.com', + }, + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, + { + protocol: 'https', + hostname: 'img.clerk.com', + }, + { + protocol: 'https', + hostname: 'firebasestorage.googleapis.com', }, ], }, diff --git a/nixpacks.toml b/nixpacks.toml index cb00d07..396d648 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -2,10 +2,18 @@ nixPkgs = ["nodejs_20"] [phases.install] -cmds = ["npm ci --legacy-peer-deps"] +cmds = ["npm install --legacy-peer-deps"] [phases.build] -cmds = ["npm run build"] +cmds = [ + "rm -rf .next/cache", + "rm -rf node_modules/.cache", + "npm run build" +] + +[variables] +SKIP_ENV_VALIDATION = "true" +NODE_ENV = "production" [start] cmd = "npm start" diff --git a/package.json b/package.json index 382f5c9..88d56aa 100644 --- a/package.json +++ b/package.json @@ -100,4 +100,4 @@ "tailwindcss": "^4", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/src/app/LandingPageClient.tsx b/src/app/LandingPageClient.tsx new file mode 100644 index 0000000..aa5ee5a --- /dev/null +++ b/src/app/LandingPageClient.tsx @@ -0,0 +1,112 @@ +"use client"; + +import nextDynamic from 'next/dynamic'; +import { Suspense } from 'react'; +import { Header, HeroSection, HeroHighlightSection, StickyScrollRevealDemo } from '@/components/layout'; +import { StickyFooter } from "@/components/ui/sticky-footer"; + +// Loading skeleton for heavy sections +const SectionSkeleton = ({ height = "h-[500px]" }: { height?: string }) => ( +
${slide.content}
` : ""}{slide.title}
{deck.tagline || "No tagline"}
+- Note: GitHub connection is required for Code Police (code review) + Note: GitHub connection is required for Code Police (code review) and Pitch Deck Generator (repository analysis). Click "Connect GitHub" above to get started.
+ Join thousands of teams already using GhostHunter to build better products. +
+
{
whileTap="whileTap"
className="rounded-xl -mr-4 mt-4 p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 border border-neutral-100 shrink-0 overflow-hidden"
>
-