Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
177 changes: 177 additions & 0 deletions src/security.js
Original file line number Diff line number Diff line change
@@ -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!'));
}
9 changes: 0 additions & 9 deletions templates/portfolio/nextjs-monolith/env.mjs

This file was deleted.

17 changes: 0 additions & 17 deletions templates/portfolio/nextjs-monolith/lib/db.ts

This file was deleted.

Loading