diff --git a/src/generate.js b/src/generate.js index 2147477..1ca99ed 100644 --- a/src/generate.js +++ b/src/generate.js @@ -8,6 +8,7 @@ import Handlebars from 'handlebars'; import { execSync } from 'child_process'; import { resolveDependencies } from './dependencies.js'; import { generateNavigation } from './navigation.js'; +import { applySecurity } from './security.js'; // Setup __dirname for ES Modules to fix the local template path bug const __filename = fileURLToPath(import.meta.url); @@ -160,7 +161,10 @@ export async function generateProject(config) { // 6. Resolve dynamic dependencies based on user choices resolveDependencies(projectPath, config); - // 6. AUTOMATION PHASE: Install Dependencies + // 7. Apply security hardening if enabled + applySecurity(projectPath, config); + + // 8. AUTOMATION PHASE: Install Dependencies if (config.noInstall) { console.log(chalk.gray('\n⏭️ Skipping npm install (--no-install).')); } else { diff --git a/src/security.js b/src/security.js index e69de29..ddfd818 100644 --- a/src/security.js +++ b/src/security.js @@ -0,0 +1,177 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; + +const ENV_MJS_CONTENT = `import { z } from 'zod'; + +/** + * Environment variable validation using Zod. + * This ensures all required env vars are present and correctly formatted + * at build time — not silently failing at runtime. + * + * Generated by Opusify CLI with Enterprise Security Hardening enabled. + */ +const envSchema = z.object({ + // Database + DATABASE_URL: z.string().url().optional(), + + // Authentication + NEXTAUTH_SECRET: z.string().min(32).optional(), + NEXTAUTH_URL: z.string().url().optional(), + + // Payments (optional) + STRIPE_SECRET_KEY: z.string().startsWith('sk_').optional(), + STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_').optional(), + + // App + NEXT_PUBLIC_APP_URL: z.string().url().optional(), +}); + +export const env = envSchema.parse(process.env); +`; + +const ENV_EXAMPLE_CONTENT = `# ============================================================================= +# Environment Variables +# Generated by Opusify CLI — Enterprise Security Hardening +# ============================================================================= +# Copy this file to .env.local and fill in your values: +# cp .env.example .env.local +# ============================================================================= + +# Database Connection +# Format: postgresql://user:password@host:port/database +DATABASE_URL= + +# NextAuth.js Authentication +# Generate a secret: openssl rand -base64 32 +NEXTAUTH_SECRET= +NEXTAUTH_URL=http://localhost:3000 + +# Stripe Payments (optional) +# Get keys from: https://dashboard.stripe.com/apikeys +STRIPE_SECRET_KEY= +STRIPE_PUBLISHABLE_KEY= + +# Public App URL +NEXT_PUBLIC_APP_URL=http://localhost:3000 +`; + +const NEXT_CONFIG_SECURE = `/** @type {import('next').NextConfig} */ +const nextConfig = { + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { + key: 'Content-Security-Policy', + value: [ + "default-src 'self'", + "script-src 'self' 'unsafe-eval' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self' data:", + "connect-src 'self' https:", + "frame-ancestors 'none'", + ].join('; '), + }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', + }, + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + ], + }, + ]; + }, +}; + +module.exports = nextConfig; +`; + +export function applySecurity(projectPath, config) { + if (!config.enableSecurity) return; + + const isNextjs = config.architecture === 'nextjs-monolith' || config.architecture === 'nextjs-turborepo'; + const verbose = config.verbose || false; + + if (!isNextjs) { + if (verbose) { + console.log(chalk.gray(' [security] Skipped — security hardening only applies to Next.js projects')); + } + return; + } + + console.log(chalk.cyan('\n🔒 Applying Enterprise Security Hardening...')); + + // 1. Generate env.mjs + const envMjsPath = path.join(projectPath, 'env.mjs'); + fs.writeFileSync(envMjsPath, ENV_MJS_CONTENT); + if (verbose) console.log(chalk.gray(' [security] Created env.mjs (Zod validation)')); + + // 2. Generate .env.example + const envExamplePath = path.join(projectPath, '.env.example'); + fs.writeFileSync(envExamplePath, ENV_EXAMPLE_CONTENT); + if (verbose) console.log(chalk.gray(' [security] Created .env.example')); + + // 3. Replace next.config.js with security headers + const nextConfigPath = path.join(projectPath, 'next.config.js'); + fs.writeFileSync(nextConfigPath, NEXT_CONFIG_SECURE); + if (verbose) console.log(chalk.gray(' [security] Updated next.config.js with CSP headers')); + + // 4. Add .gitignore entries for env files + const gitignorePath = path.join(projectPath, '.gitignore'); + const gitignoreContent = `# Dependencies +node_modules/ + +# Environment variables +.env +.env.local +.env.production +.env.*.local + +# Next.js +.next/ +out/ + +# Build +dist/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db +`; + fs.writeFileSync(gitignorePath, gitignoreContent); + if (verbose) console.log(chalk.gray(' [security] Created .gitignore')); + + // 5. Add zod to package.json + const pkgPath = path.join(projectPath, 'package.json'); + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + if (!pkg.dependencies) pkg.dependencies = {}; + pkg.dependencies['zod'] = '^3.23.8'; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); + if (verbose) console.log(chalk.gray(' [security] Added zod@^3.23.8 to dependencies')); + } + + console.log(chalk.green(' ✔ Security hardening applied!')); +} diff --git a/templates/portfolio/nextjs-monolith/env.mjs b/templates/portfolio/nextjs-monolith/env.mjs deleted file mode 100644 index 64f12a2..0000000 --- a/templates/portfolio/nextjs-monolith/env.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod'; - -const envSchema = z.object({ - DATABASE_URL: z.string().url().optional(), - NEXTAUTH_SECRET: z.string().min(32).optional(), - NEXTAUTH_URL: z.string().url().optional(), -}); - -export const env = envSchema.parse(process.env); diff --git a/templates/portfolio/nextjs-monolith/lib/db.ts b/templates/portfolio/nextjs-monolith/lib/db.ts deleted file mode 100644 index 7a4f528..0000000 --- a/templates/portfolio/nextjs-monolith/lib/db.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { env } from '../env.mjs'; - -/** - * Database connection using validated environment variables. - * The `env` object is validated at build time via Zod (see env.mjs). - * This ensures DATABASE_URL is always defined and correctly formatted. - */ - -const globalForDb = globalThis as unknown as { - dbUrl: string | undefined; -}; - -export const databaseUrl = globalForDb.dbUrl ?? env.DATABASE_URL; - -if (process.env.NODE_ENV !== 'production') { - globalForDb.dbUrl = databaseUrl; -}