← back to Letsbegin

app/api/projects/scan/route.ts

405 lines

import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'

// Admin-only: Check for admin header or IP
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD
if (!ADMIN_PASSWORD) {
  throw new Error('ADMIN_PASSWORD environment variable is required')
}
const ADMIN_IPS = ['127.0.0.1', '::1', '45.61.58.125', 'localhost']

function isAdmin(request: NextRequest): boolean {
  // Check admin header
  const authHeader = request.headers.get('x-admin-key')
  if (authHeader === ADMIN_PASSWORD) return true

  // Check for internal/same-origin requests (browser requests from the app itself)
  const referer = request.headers.get('referer')
  const host = request.headers.get('host')
  if (referer && host && referer.includes(host)) return true

  // Check X-Forwarded-For
  const forwardedFor = request.headers.get('x-forwarded-for')
  const ip = forwardedFor?.split(',')[0]?.trim() || ''
  if (ADMIN_IPS.includes(ip)) return true

  // Allow localhost connections
  const url = new URL(request.url)
  if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') return true

  return false
}

interface GitInfo {
  isRepo: boolean
  branch: string | null
  remoteUrl: string | null
  lastCommit: string | null
  lastCommitDate: string | null
  uncommittedChanges: number
  hasGitignore: boolean
}

interface SecurityFeatures {
  hasAuth: boolean
  hasEnvExample: boolean
  hasEnvLocal: boolean
  hasSecrets: boolean
  authType: string | null
  hasHttps: boolean
  hasCors: boolean
  hasRateLimiting: boolean
  hasInputValidation: boolean
  hasCSRF: boolean
}

interface DetectedAPI {
  name: string
  type: 'ai' | 'payment' | 'storage' | 'auth' | 'email' | 'analytics' | 'other'
  detected: boolean
  envVar?: string
}

interface ProjectScanResult {
  name: string
  path: string
  description: string
  techStack: string[]
  dependencies: string[]
  components: string[]
  existingPRDs: string[]
  hasReadme: boolean
  hasClaudeMd: boolean
  hasPackageJson: boolean
  // Enhanced Git Info
  git: GitInfo
  // Security Features
  security: SecurityFeatures
  // Detected APIs
  detectedAPIs: DetectedAPI[]
  recentFiles: string[]
  suggestedFeatures: string[]
}

export async function GET(request: NextRequest) {
  // Check admin access
  if (!isAdmin(request)) {
    return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
  }

  const { searchParams } = new URL(request.url)
  const projectPath = searchParams.get('path')

  if (!projectPath) {
    return NextResponse.json({ error: 'Project path required' }, { status: 400 })
  }

  if (!fs.existsSync(projectPath)) {
    return NextResponse.json({ error: 'Project not found' }, { status: 404 })
  }

  try {
    const result = await scanProject(projectPath)
    return NextResponse.json(result)
  } catch (error) {
    console.error('Failed to scan project:', error)
    return NextResponse.json({ error: 'Failed to scan project' }, { status: 500 })
  }
}

async function scanProject(projectPath: string): Promise<ProjectScanResult> {
  const name = path.basename(projectPath)
  const result: ProjectScanResult = {
    name,
    path: projectPath,
    description: '',
    techStack: [],
    dependencies: [],
    components: [],
    existingPRDs: [],
    hasReadme: false,
    hasClaudeMd: false,
    hasPackageJson: false,
    git: {
      isRepo: false,
      branch: null,
      remoteUrl: null,
      lastCommit: null,
      lastCommitDate: null,
      uncommittedChanges: 0,
      hasGitignore: false
    },
    security: {
      hasAuth: false,
      hasEnvExample: false,
      hasEnvLocal: false,
      hasSecrets: false,
      authType: null,
      hasHttps: false,
      hasCors: false,
      hasRateLimiting: false,
      hasInputValidation: false,
      hasCSRF: false
    },
    detectedAPIs: [],
    recentFiles: [],
    suggestedFeatures: []
  }

  // Check for package.json
  const packageJsonPath = path.join(projectPath, 'package.json')
  if (fs.existsSync(packageJsonPath)) {
    result.hasPackageJson = true
    try {
      const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
      result.description = pkg.description || ''

      // Extract tech stack from dependencies
      const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
      if (allDeps.next) result.techStack.push(`Next.js ${allDeps.next.replace('^', '')}`)
      if (allDeps.react) result.techStack.push(`React ${allDeps.react.replace('^', '')}`)
      if (allDeps.typescript) result.techStack.push('TypeScript')
      if (allDeps.tailwindcss) result.techStack.push('Tailwind CSS')
      if (allDeps.prisma || allDeps['@prisma/client']) result.techStack.push('Prisma')
      if (allDeps.express) result.techStack.push('Express.js')
      if (allDeps['@anthropic-ai/sdk']) result.techStack.push('Claude API')

      result.dependencies = Object.keys(allDeps).slice(0, 20)
    } catch (e) {
      console.error('Failed to parse package.json:', e)
    }
  }

  // Check for README.md
  const readmePath = path.join(projectPath, 'README.md')
  if (fs.existsSync(readmePath)) {
    result.hasReadme = true
    try {
      const readme = fs.readFileSync(readmePath, 'utf8')
      // Extract first paragraph as description if not set
      if (!result.description) {
        const lines = readme.split('\n').filter(l => l.trim() && !l.startsWith('#'))
        result.description = lines[0]?.slice(0, 200) || ''
      }
    } catch (e) {
      console.error('Failed to read README:', e)
    }
  }

  // Check for CLAUDE.md
  const claudeMdPaths = [
    path.join(projectPath, '.claude', 'CLAUDE.md'),
    path.join(projectPath, 'CLAUDE.md')
  ]
  for (const p of claudeMdPaths) {
    if (fs.existsSync(p)) {
      result.hasClaudeMd = true
      break
    }
  }

  // Check for git - Enhanced
  const gitPath = path.join(projectPath, '.git')
  result.git.hasGitignore = fs.existsSync(path.join(projectPath, '.gitignore'))

  if (fs.existsSync(gitPath)) {
    result.git.isRepo = true
    try {
      const { exec } = await import('child_process')
      const { promisify } = await import('util')
      const execAsync = promisify(exec)

      // Get branch
      try {
        const { stdout: branch } = await execAsync(`cd "${projectPath}" && git branch --show-current`)
        result.git.branch = branch.trim() || 'detached'
      } catch { result.git.branch = 'unknown' }

      // Get remote URL
      try {
        const { stdout: remote } = await execAsync(`cd "${projectPath}" && git remote get-url origin 2>/dev/null`)
        result.git.remoteUrl = remote.trim() || null
      } catch { /* no remote */ }

      // Get last commit
      try {
        const { stdout: commit } = await execAsync(`cd "${projectPath}" && git log -1 --format="%s" 2>/dev/null`)
        result.git.lastCommit = commit.trim() || null
      } catch { /* no commits */ }

      // Get last commit date
      try {
        const { stdout: date } = await execAsync(`cd "${projectPath}" && git log -1 --format="%ci" 2>/dev/null`)
        result.git.lastCommitDate = date.trim() || null
      } catch { /* no commits */ }

      // Get uncommitted changes count
      try {
        const { stdout: status } = await execAsync(`cd "${projectPath}" && git status --porcelain 2>/dev/null`)
        result.git.uncommittedChanges = status.trim().split('\n').filter(Boolean).length
      } catch { /* ignore */ }
    } catch (e) {
      console.error('Git scan error:', e)
    }
  }

  // Check security features
  result.security.hasEnvExample = fs.existsSync(path.join(projectPath, '.env.example'))
  result.security.hasEnvLocal = fs.existsSync(path.join(projectPath, '.env.local')) || fs.existsSync(path.join(projectPath, '.env'))

  // Scan for auth patterns in code
  try {
    const { exec } = await import('child_process')
    const { promisify } = await import('util')
    const execAsync = promisify(exec)

    // Check for auth libraries
    if (result.dependencies.includes('next-auth') || result.dependencies.includes('@auth/core')) {
      result.security.hasAuth = true
      result.security.authType = 'NextAuth'
    } else if (result.dependencies.includes('passport')) {
      result.security.hasAuth = true
      result.security.authType = 'Passport'
    } else if (result.dependencies.includes('jsonwebtoken') || result.dependencies.includes('jose')) {
      result.security.hasAuth = true
      result.security.authType = 'JWT'
    }

    // Check for CORS
    if (result.dependencies.includes('cors') || result.dependencies.includes('@fastify/cors')) {
      result.security.hasCors = true
    }

    // Check for rate limiting
    if (result.dependencies.includes('express-rate-limit') || result.dependencies.includes('rate-limiter-flexible')) {
      result.security.hasRateLimiting = true
    }

    // Check for validation
    if (result.dependencies.includes('zod') || result.dependencies.includes('joi') || result.dependencies.includes('yup')) {
      result.security.hasInputValidation = true
    }

    // Check for CSRF
    if (result.dependencies.includes('csurf') || result.dependencies.includes('csrf')) {
      result.security.hasCSRF = true
    }

    // Check for secrets/credentials in common files
    try {
      const { stdout: secretsCheck } = await execAsync(
        `grep -r "password\\|secret\\|api_key\\|apikey" "${projectPath}"/*.ts "${projectPath}"/*.js "${projectPath}"/lib/*.ts 2>/dev/null | head -5`
      )
      result.security.hasSecrets = secretsCheck.trim().length > 0
    } catch { /* no matches */ }
  } catch (e) {
    console.error('Security scan error:', e)
  }

  // Detect APIs from dependencies and env files
  const apiDetections: DetectedAPI[] = [
    { name: 'Claude/Anthropic', type: 'ai', detected: result.dependencies.includes('@anthropic-ai/sdk'), envVar: 'ANTHROPIC_API_KEY' },
    { name: 'OpenAI', type: 'ai', detected: result.dependencies.includes('openai'), envVar: 'OPENAI_API_KEY' },
    { name: 'Google AI', type: 'ai', detected: result.dependencies.includes('@google/generative-ai'), envVar: 'GOOGLE_AI_KEY' },
    { name: 'Stripe', type: 'payment', detected: result.dependencies.includes('stripe'), envVar: 'STRIPE_SECRET_KEY' },
    { name: 'PayPal', type: 'payment', detected: result.dependencies.includes('@paypal/checkout-server-sdk'), envVar: 'PAYPAL_CLIENT_ID' },
    { name: 'AWS S3', type: 'storage', detected: result.dependencies.includes('@aws-sdk/client-s3'), envVar: 'AWS_ACCESS_KEY_ID' },
    { name: 'Cloudinary', type: 'storage', detected: result.dependencies.includes('cloudinary'), envVar: 'CLOUDINARY_URL' },
    { name: 'Firebase', type: 'auth', detected: result.dependencies.includes('firebase') || result.dependencies.includes('firebase-admin'), envVar: 'FIREBASE_API_KEY' },
    { name: 'Auth0', type: 'auth', detected: result.dependencies.includes('@auth0/nextjs-auth0'), envVar: 'AUTH0_SECRET' },
    { name: 'SendGrid', type: 'email', detected: result.dependencies.includes('@sendgrid/mail'), envVar: 'SENDGRID_API_KEY' },
    { name: 'Resend', type: 'email', detected: result.dependencies.includes('resend'), envVar: 'RESEND_API_KEY' },
    { name: 'Twilio', type: 'email', detected: result.dependencies.includes('twilio'), envVar: 'TWILIO_ACCOUNT_SID' },
    { name: 'Shopify', type: 'other', detected: result.dependencies.includes('@shopify/shopify-api'), envVar: 'SHOPIFY_ACCESS_TOKEN' },
    { name: 'Slack', type: 'other', detected: result.dependencies.includes('@slack/web-api'), envVar: 'SLACK_BOT_TOKEN' },
    { name: 'Google Analytics', type: 'analytics', detected: result.dependencies.includes('@google-analytics/data'), envVar: 'GA_MEASUREMENT_ID' },
    { name: 'Mixpanel', type: 'analytics', detected: result.dependencies.includes('mixpanel'), envVar: 'MIXPANEL_TOKEN' },
    { name: 'Sentry', type: 'analytics', detected: result.dependencies.includes('@sentry/nextjs'), envVar: 'SENTRY_DSN' },
    { name: 'Prisma', type: 'storage', detected: result.dependencies.includes('@prisma/client'), envVar: 'DATABASE_URL' },
    { name: 'ElevenLabs', type: 'ai', detected: result.dependencies.includes('elevenlabs'), envVar: 'ELEVENLABS_API_KEY' },
    { name: 'Zendesk', type: 'other', detected: result.dependencies.includes('zendesk-api'), envVar: 'ZENDESK_API_TOKEN' },
  ]

  result.detectedAPIs = apiDetections.filter(api => api.detected)

  // Scan components directory
  const componentsDirs = ['components', 'src/components', 'app/components']
  for (const dir of componentsDirs) {
    const dirPath = path.join(projectPath, dir)
    if (fs.existsSync(dirPath)) {
      try {
        const files = fs.readdirSync(dirPath, { withFileTypes: true })
        for (const file of files) {
          if (file.isFile() && (file.name.endsWith('.tsx') || file.name.endsWith('.jsx'))) {
            result.components.push(file.name)
          }
        }
      } catch (e) {
        console.error('Failed to scan components:', e)
      }
      break
    }
  }

  // Scan for existing PRDs
  const tasksDir = path.join(projectPath, 'tasks')
  if (fs.existsSync(tasksDir)) {
    try {
      const files = fs.readdirSync(tasksDir)
      result.existingPRDs = files.filter(f => f.startsWith('prd-') && f.endsWith('.md'))
    } catch (e) {
      console.error('Failed to scan tasks:', e)
    }
  }

  // Get recently modified files
  try {
    const { exec } = await import('child_process')
    const { promisify } = await import('util')
    const execAsync = promisify(exec)
    const { stdout } = await execAsync(
      `find "${projectPath}" -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" 2>/dev/null | head -20 | xargs ls -t 2>/dev/null | head -10`
    )
    result.recentFiles = stdout.trim().split('\n').filter(Boolean).map(f => path.relative(projectPath, f))
  } catch (e) {
    // Ignore errors
  }

  // Generate suggested features based on project analysis
  result.suggestedFeatures = generateFeatureSuggestions(result)

  return result
}

function generateFeatureSuggestions(project: ProjectScanResult): string[] {
  const suggestions: string[] = []

  if (!project.hasReadme) {
    suggestions.push('Add comprehensive README documentation')
  }
  if (!project.hasClaudeMd) {
    suggestions.push('Create CLAUDE.md for AI-assisted development')
  }
  if (project.techStack.includes('Next.js') && !project.components.some(c => c.includes('Loading'))) {
    suggestions.push('Add loading states and skeletons')
  }
  if (project.techStack.includes('Next.js') && !project.components.some(c => c.includes('Error'))) {
    suggestions.push('Add error boundary components')
  }
  if (project.components.length > 10) {
    suggestions.push('Refactor and organize component structure')
  }
  if (project.techStack.includes('Tailwind CSS')) {
    suggestions.push('Add dark mode support')
  }
  if (project.dependencies.includes('next-auth') || project.dependencies.includes('auth')) {
    suggestions.push('Enhance authentication security')
  }
  if (!project.dependencies.includes('jest') && !project.dependencies.includes('vitest')) {
    suggestions.push('Add unit testing framework')
  }

  return suggestions.slice(0, 5)
}