diff --git a/.env.example b/.env.example index a846aaf..2fda82d 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ DOCKER_ENV=false # API runtime / compose defaults DATABASE_URL=postgresql://oricms:oricms@localhost:5432/oricms -JWT_SECRET=change-me-with-at-least-32-characters +JWT_SECRET=REPLACE_ME_WITH_AT_LEAST_32_CHARACTERS GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= @@ -23,7 +23,7 @@ APP_BASE_URL=http://localhost:5173 TRUST_PROXY=1 # Production deployments should point this at a shared Redis instance so rate limits # remain consistent across multiple API instances. -RATE_LIMIT_REDIS_URL=redis://redis:6379/0 +RATE_LIMIT_REDIS_URL= RATE_LIMIT_REDIS_PREFIX=oricms:rate-limit # Encryption key used by the API for AES-256-GCM encryption of sensitive data @@ -32,4 +32,4 @@ RATE_LIMIT_REDIS_PREFIX=oricms:rate-limit # node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # Keep packages/api/.env aligned with the API runtime values you actually use. # If you previously deployed with the old example key, rotate immediately. -ENCRYPTION_KEY= +ENCRYPTION_KEY=REPLACE_ME_64_HEX_CHARS_REQUIRED_FOR_DEVELOPMENT_ONLY diff --git a/README.md b/README.md index 4a4306d..bd65f00 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ examples/ Runnable and copyable examples Requirements: -- Node.js 20+ +- Node.js 20+ (use `.nvmrc` if you use nvm) - npm 10+ - Docker - Git @@ -55,6 +55,7 @@ Requirements: Setup: ```bash +nvm use node scripts/setup.mjs ``` diff --git a/docs/developer/local-development.md b/docs/developer/local-development.md index 509a66a..a369d4d 100644 --- a/docs/developer/local-development.md +++ b/docs/developer/local-development.md @@ -4,7 +4,7 @@ Use this guide when you are setting up a new OriCMS worktree or returning to loc ## Runtime Requirements -- Node.js 20+ +- Node.js 20+ (use `.nvmrc` if you use nvm) - npm 10+ - Docker - Git diff --git a/packages/api/.env.example b/packages/api/.env.example index 99991f3..c2040fd 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -3,16 +3,16 @@ # Database # For Docker Compose: postgresql://oricms:oricms@postgres:5432/oricms -# For local development: postgresql://user:password@localhost:5432/oricms -DATABASE_URL=postgresql://localhost:5432/oricms +# For local development: postgresql://oricms:oricms@localhost:5432/oricms +DATABASE_URL=postgresql://oricms:oricms@localhost:5432/oricms # JWT Secret (min 32 characters) # Generate with: openssl rand -hex 32 -JWT_SECRET=your-jwt-secret-change-in-production +JWT_SECRET=REPLACE_ME_WITH_AT_LEAST_32_CHARACTERS # Encryption Key (32 bytes, 64 hex characters) # Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -ENCRYPTION_KEY=your-encryption-key-change-in-production +ENCRYPTION_KEY=REPLACE_ME_64_HEX_CHARS_REQUIRED_FOR_DEVELOPMENT_ONLY # Allow creating sites with file:// repo URLs (development/testing only) # Keep unset or false in production. diff --git a/scripts/setup.mjs b/scripts/setup.mjs index 46e8d98..bffea56 100755 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -27,6 +27,16 @@ const args = new Set(process.argv.slice(2)); const SKIP_DOCKER = args.has("--skip-docker"); const RESET_DB = args.has("--reset-db"); +// Detect available docker compose command (v2 plugin vs legacy) +const DOCKER_COMPOSE = await (async () => { + try { + await run("docker", ["compose", "version"], { silent: true, ignoreError: false }); + return ["docker", "compose"]; + } catch { + return ["docker-compose"]; + } +})(); + function log(msg) { console.log(msg); } @@ -169,6 +179,37 @@ async function copyEnvFiles() { } } +function isValidHexKey(key, length) { + return typeof key === "string" && key.length === length && /^[0-9a-fA-F]+$/.test(key); +} + +async function validateEnvFiles() { + const rootEnvPath = ".env"; + const apiEnvPath = "packages/api/.env"; + + for (const envPath of [rootEnvPath, apiEnvPath]) { + if (!existsSync(envPath)) continue; + + const content = (await import("node:fs")).readFileSync(envPath, "utf-8"); + const lines = content.split("\n"); + const vars = {}; + for (const line of lines) { + const match = line.match(/^([A-Za-z0-9_]+)=(.*)$/); + if (match) vars[match[1]] = match[2]; + } + + const jwt = vars.JWT_SECRET; + if (!jwt || jwt.length < 32 || jwt.includes("change") || jwt.includes("your-")) { + warn(`${envPath}: JWT_SECRET is missing or looks like a placeholder`); + } + + const enc = vars.ENCRYPTION_KEY; + if (!isValidHexKey(enc, 64)) { + warn(`${envPath}: ENCRYPTION_KEY is missing or not a 64-character hex string`); + } + } +} + async function startPostgres() { if (SKIP_DOCKER) { info("Skipping Postgres startup (--skip-docker)"); @@ -177,6 +218,31 @@ async function startPostgres() { info("Starting Postgres via Docker..."); + // Check for existing container from a previous run or different worktree + try { + const { stdout } = await run( + "docker", + ["ps", "-a", "--filter", "name=^/oricms-postgres$", "--format", "{{.Names}}"], + { silent: true, ignoreError: true } + ); + if (stdout.trim() === "oricms-postgres") { + const { stdout: status } = await run( + "docker", + ["inspect", "--format", "{{.State.Status}}", "oricms-postgres"], + { silent: true } + ); + if (status.trim() === "running" && !RESET_DB) { + info("Existing oricms-postgres container is already running; reusing it"); + success("Postgres is ready"); + return; + } + info("Removing existing oricms-postgres container..."); + await run("docker", ["rm", "-f", "oricms-postgres"], { silent: true, ignoreError: true }); + } + } catch { + // ignore — proceed to normal startup + } + if (RESET_DB) { info("Removing existing database container (--reset-db)..."); try { @@ -186,7 +252,7 @@ async function startPostgres() { } } - await run("docker-compose", ["up", "-d", "postgres"]); + await run(DOCKER_COMPOSE[0], [...DOCKER_COMPOSE.slice(1), "up", "-d", "postgres"]); // Wait for Postgres to be ready info("Waiting for Postgres to be ready..."); @@ -258,6 +324,7 @@ async function main() { step(++currentStep, TOTAL_STEPS, "Copying environment files"); await copyEnvFiles(); + await validateEnvFiles(); step(++currentStep, TOTAL_STEPS, "Starting Postgres"); await startPostgres();