Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/railway-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 65 additions & 2 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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 = {
Expand All @@ -39,3 +101,4 @@ export const config = {
"/(api|trpc)(.*)",
],
};

20 changes: 18 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
],
},
Expand Down
12 changes: 10 additions & 2 deletions nixpacks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@
"tailwindcss": "^4",
"typescript": "^5"
}
}
}
112 changes: 112 additions & 0 deletions src/app/LandingPageClient.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div className={`${height} w-full flex items-center justify-center bg-black`}>
<div className="w-8 h-8 border-2 border-zinc-500 border-t-white rounded-full animate-spin" />
</div>
);

// Dynamic imports with loading states for heavy components
const FeaturesSectionDemo = nextDynamic(
() => import("@/components/ui/features-section-demo-3"),
{
loading: () => <SectionSkeleton height="h-[600px]" />,
ssr: true,
}
);

const AnimatedTestimonialsDemo = nextDynamic(
() => import("@/components/ui/animated-testimonials-demo"),
{
loading: () => <SectionSkeleton height="h-[400px]" />,
ssr: false, // Disable SSR for client-only animations
}
);

const SplineSceneDemo = nextDynamic(
() => import("@/components/ui/spline-scene-demo").then(mod => ({ default: mod.SplineSceneDemo })),
{
loading: () => <SectionSkeleton />,
ssr: false, // Spline is client-only
}
);

const TextHoverEffect = nextDynamic(
() => import("@/components/ui/text-hover-effect").then(mod => ({ default: mod.TextHoverEffect })),
{
loading: () => <SectionSkeleton height="h-[20rem]" />,
ssr: false,
}
);

const CallToAction = nextDynamic(
() => import("@/components/ui/cta").then(mod => ({ default: mod.CallToAction })),
{
loading: () => <SectionSkeleton height="h-[200px]" />,
ssr: true,
}
);

export default function LandingPageClient() {
return (
<div className="min-h-screen bg-black dark:bg-black">
<Header />

{/* Hero Section with Background Ripple Effect */}
<HeroSection />

{/* Text Highlight Section */}
<HeroHighlightSection
text="Enough Building, time for"
highlightedText="redemption."
/>

{/* Interactive 3D Spline Scene - Lazy loaded */}
<section className="py-16 px-4 max-w-7xl mx-auto">
<Suspense fallback={<SectionSkeleton />}>
<SplineSceneDemo />
</Suspense>
</section>

{/* Sticky Scroll Features Section */}
<StickyScrollRevealDemo />

{/* Features Section - Contains heavy Globe component */}
<Suspense fallback={<SectionSkeleton height="h-[600px]" />}>
<FeaturesSectionDemo />
</Suspense>

{/* GHOSTFOUNDER Text Effect */}
<section className="py-8 flex items-center justify-center">
<Suspense fallback={<SectionSkeleton height="h-[20rem]" />}>
<TextHoverEffect
text="GHOSTFOUNDER"
containerHeight="20rem"
viewBox="0 0 500 100"
/>
</Suspense>
</section>

{/* Animated Testimonials Section */}
<Suspense fallback={<SectionSkeleton height="h-[400px]" />}>
<AnimatedTestimonialsDemo />
</Suspense>

{/* CTA Section */}
<section className="py-20 px-4">
<Suspense fallback={<SectionSkeleton height="h-[200px]" />}>
<CallToAction />
</Suspense>
</section>

{/* Sticky Footer Reveal */}
<StickyFooter />
</div>
);
}
55 changes: 7 additions & 48 deletions src/app/api/code-police/analyze/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@ import type { DocumentData, QueryDocumentSnapshot, Firestore } from "firebase-ad
* Analyzes code from a GitHub repository and optionally sends email report.
*/

// Helper to remove undefined values for Firestore
const sanitizeForFirestore = <T extends Record<string, unknown>>(obj: T): T => {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (value !== undefined) {
result[key] = value;
}
}
return result as T;
};

export async function POST(request: NextRequest) {
try {
const { userId } = await auth();
Expand Down Expand Up @@ -125,38 +114,8 @@ export async function POST(request: NextRequest) {
createdAt: now,
});

// Fetch commit details - if no SHA provided, get latest commit
let commit;
let actualCommitSha = commitSha;

if (!commitSha || commitSha === "latest") {
console.log("[Analyze] No commit SHA provided, fetching latest commit from main branch...");
// Fetch the list of commits to get the latest SHA
const commitsResponse = await fetch(
`https://api.github.com/repos/${owner}/${repo}/commits?per_page=1`,
{
headers: {
Authorization: `Bearer ${githubToken}`,
Accept: "application/vnd.github.v3+json",
},
}
);

if (!commitsResponse.ok) {
const errorData = await commitsResponse.json().catch(() => ({}));
throw new Error(`Failed to fetch commits: ${commitsResponse.status} ${errorData.message || commitsResponse.statusText}`);
}

const commits = await commitsResponse.json();
if (!commits || commits.length === 0) {
throw new Error("No commits found in repository");
}

actualCommitSha = commits[0].sha;
console.log("[Analyze] Using latest commit:", actualCommitSha);
}

commit = await fetchCommit(githubToken, owner, repo, actualCommitSha);
// Fetch commit details
const commit = await fetchCommit(githubToken, owner, repo, commitSha);

// ========================================================================
// FILE FILTERING - Exclude non-source files from analysis
Expand Down Expand Up @@ -240,7 +199,7 @@ export async function POST(request: NextRequest) {

// Ensure commit.files exists
const commitFiles = commit.files || [];
console.log(`[Analyze] Commit ${actualCommitSha}: ${commitFiles.length} files changed`);
console.log(`[Analyze] Commit ${commitSha}: ${commitFiles.length} files changed`);

if (commitFiles.length === 0) {
console.log(`[Analyze] ⚠️ No files in commit to analyze`);
Expand All @@ -263,7 +222,7 @@ export async function POST(request: NextRequest) {
owner,
repo,
file.filename,
actualCommitSha
commitSha
);

// Skip very large files (> 50KB) to avoid token limits
Expand Down Expand Up @@ -317,13 +276,13 @@ export async function POST(request: NextRequest) {
const batch = adminDb.batch();
for (const issue of fullIssues) {
const issueRef = analysisRef.collection("issues").doc(issue.id);
batch.set(issueRef, sanitizeForFirestore(issue as unknown as Record<string, unknown>));
batch.set(issueRef, issue);
}

// Generate summary
const summary = await generateAnalysisSummary({
repoName: `${owner}/${repo}`,
commitSha: actualCommitSha,
commitSha,
branch: "main",
issues: fullIssues,
});
Expand Down Expand Up @@ -369,7 +328,7 @@ export async function POST(request: NextRequest) {
issues: fullIssues,
summary,
repoName: `${owner}/${repo}`,
commitUrl: `https://github.com/${owner}/${repo}/commit/${actualCommitSha}`,
commitUrl: `https://github.com/${owner}/${repo}/commit/${commitSha || 'HEAD'}`,
});

await analysisRef.update({ emailStatus: "sent", emailSentTo: emailTo });
Expand Down
Loading
Loading