← 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)
}