Files
office_translator/scripts/setup.js
sepehr 98d82414bb
Some checks failed
Deploy to Homelab / Deploy Wordly to 192.168.1.151 (push) Has been cancelled
Deploy to Homelab / Deploy Monitoring (if configured) (push) Has been cancelled
feat: add Node.js setup wizard (replaces broken bash scripts)
Node.js script that actually works:
- Generates JWT_SECRET_KEY, ADMIN_TOKEN_SECRET, POSTGRES_PASSWORD
- Hashes admin password with bcrypt via docker
- Writes ADMIN_PASSWORD (plaintext) + ADMIN_PASSWORD_HASH (bcrypt)
- Sets *_ENABLED=true for chosen provider, false for others
- Writes ALL provider fields (api_key, model, base_url)
- Optionally configures Stripe
- Optionally starts docker compose
- Verified: syntax OK, all env vars match backend expectations

Usage: node scripts/setup.js

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:29:28 +02:00

391 lines
14 KiB
JavaScript

#!/usr/bin/env node
/**
* Wordly.art - Setup Wizard (Node.js)
*
* Usage: node scripts/setup.js
*
* Ce script :
* 1. Pose des questions interactives
* 2. Genere les secrets
* 3. Hash le mot de passe admin en bcrypt
* 4. Ecrit le fichier .env
* 5. Lance docker compose
*/
const readline = require('readline');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ENV_FILE = path.join(__dirname, '..', '.env');
// ============================================
// Helpers
// ============================================
function rl() {
return readline.createInterface({ input: process.stdin, output: process.stdout });
}
function question(prompt, defaultVal) {
return new Promise(resolve => {
const r = rl();
const display = defaultVal ? `${prompt} [${defaultVal}]: ` : `${prompt}: `;
r.question(display, answer => {
r.close();
resolve(answer.trim() || (defaultVal || ''));
});
});
}
function questionPassword(prompt) {
return new Promise(resolve => {
const r = rl();
r.question(`${prompt}: `, answer => {
r.close();
resolve(answer);
});
// Hide input
if (process.stdin.isTTY) {
process.stdout.write('\x1b[8m'); // Hide
}
}).finally(() => {
if (process.stdout.isTTY) {
process.stdout.write('\x1b[0m'); // Show
}
});
}
function questionPasswordSimple(prompt) {
return new Promise(resolve => {
const r = readline.createInterface({ input: process.stdin, output: process.stdout });
r.question(`${prompt}: `, answer => {
r.close();
resolve(answer);
});
});
}
function generateSecret(len = 64) {
return crypto.randomBytes(len).toString('base64url').slice(0, len);
}
function generateHex(len = 32) {
return crypto.randomBytes(len).toString('hex');
}
function hashBcrypt(password) {
try {
const safePass = password.replace(/'/g, "'\"'\"'");
const cmd = 'docker run --rm python:3.12-slim bash -c "' +
"pip install 'passlib[bcrypt]' bcrypt >/dev/null 2>&1 && " +
"python3 -c \\\"from passlib.context import CryptContext; " +
"print(CryptContext(schemes=['bcrypt']).hash('" + safePass + "'))\\\"" +
'"';
const result = execSync(cmd, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });
return result.trim();
} catch (e) {
return null;
}
}
function writeEnv(content) {
fs.writeFileSync(ENV_FILE, content, 'utf-8');
// On Linux: chmod 600
try { fs.chmodSync(ENV_FILE, 0o600); } catch (e) { /* Windows */ }
}
function readEnv() {
if (!fs.existsSync(ENV_FILE)) return {};
const content = fs.readFileSync(ENV_FILE, 'utf-8');
const env = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx);
const val = trimmed.slice(eqIdx + 1);
env[key] = val;
}
return env;
}
function setEnvValues(pairs) {
let content = '';
if (fs.existsSync(ENV_FILE)) {
content = fs.readFileSync(ENV_FILE, 'utf-8');
}
for (const [key, value] of Object.entries(pairs)) {
const regex = new RegExp(`^${key}=.*$`, 'm');
if (regex.test(content)) {
content = content.replace(regex, `${key}=${value}`);
} else {
content += `\n${key}=${value}`;
}
}
writeEnv(content);
}
async function select(prompt, options) {
console.log(`\n${prompt}`);
options.forEach((opt, i) => console.log(` ${i + 1}) ${opt.label}`));
const answer = await question('Choix', '1');
const idx = parseInt(answer) - 1;
return options[Math.max(0, Math.min(idx, options.length - 1))].value;
}
// ============================================
// Main
// ============================================
async function main() {
const C = {
RED: '\x1b[0;31m', GREEN: '\x1b[0;32m', YELLOW: '\x1b[1;33m',
CYAN: '\x1b[0;36m', BOLD: '\x1b[1m', NC: '\x1b[0m',
};
console.log(`\n${C.CYAN}${C.BOLD}=========================================${C.NC}`);
console.log(`${C.CYAN}${C.BOLD} Wordly.art - Setup Wizard${C.NC}`);
console.log(`${C.CYAN}${C.BOLD}=========================================${C.NC}\n`);
// Check we're in the right directory
if (!fs.existsSync(path.join(__dirname, '..', 'docker-compose.yml'))) {
console.log(`${C.RED}Erreur: lance ce script depuis la racine du projet${C.NC}`);
console.log(`Usage: node scripts/setup.js`);
process.exit(1);
}
// Read existing .env if present
const existing = readEnv();
// ===== 1. Domaine =====
console.log(`\n${C.BOLD}--- Domaine ---${C.NC}`);
const domain = await question('Nom de domaine', existing.DOMAIN || 'wordly.art');
// ===== 2. Admin =====
console.log(`\n${C.BOLD}--- Admin ---${C.NC}`);
const adminUser = await question('Nom d\'utilisateur admin', existing.ADMIN_USERNAME || 'admin');
const adminPass = await questionPasswordSimple('Mot de passe admin');
if (!adminPass) {
console.log(`${C.RED}Mot de passe obligatoire!${C.NC}`);
process.exit(1);
}
const adminPass2 = await questionPasswordSimple('Confirmer');
if (adminPass !== adminPass2) {
console.log(`${C.RED}Ne correspondent pas!${C.NC}`);
process.exit(1);
}
console.log(`${C.GREEN}OK${C.NC}`);
// Hash bcrypt
console.log(`${C.CYAN}Hashage bcrypt...${C.NC}`);
const adminHash = hashBcrypt(adminPass);
// ===== 3. Secrets auto =====
console.log(`\n${C.BOLD}--- Secrets (auto) ---${C.NC}`);
const jwtSecret = generateSecret(64);
const adminTokenSecret = generateHex(32);
const pgPassword = generateSecret(32);
const grafanaPassword = generateSecret(24);
console.log(`${C.GREEN}JWT, Admin Token, DB, Grafana generes${C.NC}`);
// ===== 4. Provider =====
console.log(`\n${C.BOLD}--- Service de traduction ---${C.NC}`);
const providers = [
{ value: 'google', label: 'Google (gratuit, aucune cle)' },
{ value: 'deepseek', label: 'DeepSeek (~0.14$/M tokens, tres bon)' },
{ value: 'minimax', label: 'Minimax (MiniMax-M1 / m2.7)' },
{ value: 'ollama', label: 'Ollama (local, gratuit)' },
{ value: 'deepl', label: 'DeepL (haute qualite, 500k car/mois gratuit)' },
{ value: 'openai', label: 'OpenAI (GPT)' },
{ value: 'openrouter', label: 'OpenRouter (multi-modeles)' },
];
const translationService = await select('Provider par defaut :', providers);
// Provider config
const prov = {
GOOGLE_TRANSLATE_ENABLED: 'true',
OLLAMA_ENABLED: 'false', OLLAMA_BASE_URL: 'http://ollama:11434', OLLAMA_MODEL: 'llama3',
DEEPSEEK_ENABLED: 'false', DEEPSEEK_API_KEY: '', DEEPSEEK_MODEL: 'deepseek-chat', DEEPSEEK_BASE_URL: 'https://api.deepseek.com/v1',
MINIMAX_ENABLED: 'false', MINIMAX_API_KEY: '', MINIMAX_MODEL: 'MiniMax-M1', MINIMAX_BASE_URL: 'https://api.minimax.chat/v1',
DEEPL_ENABLED: 'false', DEEPL_API_KEY: '',
OPENAI_ENABLED: 'false', OPENAI_API_KEY: '', OPENAI_MODEL: 'gpt-4o-mini', OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENROUTER_ENABLED: 'false', OPENROUTER_API_KEY: '', OPENROUTER_MODEL: 'deepseek/deepseek-chat',
};
if (translationService === 'deepseek') {
prov.DEEPSEEK_ENABLED = 'true';
prov.DEEPSEEK_API_KEY = await question('Cle API DeepSeek (sk-...)', '');
prov.DEEPSEEK_MODEL = await question('Modele', 'deepseek-chat');
} else if (translationService === 'minimax') {
prov.MINIMAX_ENABLED = 'true';
prov.MINIMAX_API_KEY = await question('Cle API Minimax', '');
prov.MINIMAX_MODEL = await question('Modele', 'MiniMax-M1');
} else if (translationService === 'ollama') {
prov.OLLAMA_ENABLED = 'true';
prov.OLLAMA_BASE_URL = await question('URL Ollama', 'http://ollama:11434');
prov.OLLAMA_MODEL = await question('Modele', 'llama3');
} else if (translationService === 'deepl') {
prov.DEEPL_ENABLED = 'true';
prov.DEEPL_API_KEY = await question('Cle API DeepL', '');
} else if (translationService === 'openai') {
prov.OPENAI_ENABLED = 'true';
prov.OPENAI_API_KEY = await question('Cle API OpenAI (sk-...)', '');
prov.OPENAI_MODEL = await question('Modele', 'gpt-4o-mini');
} else if (translationService === 'openrouter') {
prov.OPENROUTER_ENABLED = 'true';
prov.OPENROUTER_API_KEY = await question('Cle API OpenRouter (sk-or-...)', '');
prov.OPENROUTER_MODEL = await question('Modele', 'deepseek/deepseek-chat');
}
// ===== 5. Stripe (optional) =====
console.log(`\n${C.BOLD}--- Stripe (optionnel) ---${C.NC}`);
const doStripe = await question('Configurer Stripe maintenant ? (oui/non)', 'non');
const stripe = { SECRET_KEY: '', WEBHOOK_SECRET: '', STARTER_PRICE_ID: '', PRO_PRICE_ID: '', BUSINESS_PRICE_ID: '' };
if (doStripe === 'oui') {
stripe.SECRET_KEY = await question('Secret Key (sk_test_... ou sk_live_...)', '');
stripe.WEBHOOK_SECRET = await question('Webhook Secret (whsec_...)', '');
stripe.STARTER_PRICE_ID = await question('Price ID Starter (price_...)', '');
stripe.PRO_PRICE_ID = await question('Price ID Pro (price_...)', '');
stripe.BUSINESS_PRICE_ID = await question('Price ID Business (price_...)', '');
}
// ===== Resume =====
console.log(`\n${C.CYAN}${C.BOLD}=========================================${C.NC}`);
console.log(`${C.CYAN}${C.BOLD} RESUME${C.NC}`);
console.log(`${C.CYAN}${C.BOLD}=========================================${C.NC}`);
console.log(` Domaine: ${domain}`);
console.log(` Admin: ${adminUser} / ${adminPass}`);
console.log(` Traduction: ${translationService}`);
console.log(` Bcrypt hash: ${adminHash ? adminHash.substring(0, 20) + '...' : 'NON GENERE (mot de passe en clair)'}`);
console.log(` Stripe: ${stripe.SECRET_KEY ? 'Configure' : 'Non configure'}`);
console.log(``);
const confirm = await question('Ecrire le .env ? (oui/non)', 'oui');
if (confirm !== 'oui') { console.log('Annule.'); process.exit(0); }
// ===== Write .env =====
const lines = [
`# Genere le ${new Date().toISOString().split('T')[0]} par setup.js`,
``,
`# ---- App ----`,
`APP_NAME=Wordly`,
`APP_ENV=production`,
`DEBUG=false`,
`LOG_LEVEL=INFO`,
``,
`# ---- Domaine ----`,
`DOMAIN=${domain}`,
`NEXT_PUBLIC_API_URL=https://${domain}`,
`BACKEND_PORT=8000`,
`FRONTEND_PORT=3000`,
``,
`# ---- Traduction ----`,
`TRANSLATION_SERVICE=${translationService}`,
``,
`GOOGLE_TRANSLATE_ENABLED=${prov.GOOGLE_TRANSLATE_ENABLED}`,
``,
`OLLAMA_ENABLED=${prov.OLLAMA_ENABLED}`,
`OLLAMA_BASE_URL=${prov.OLLAMA_BASE_URL}`,
`OLLAMA_MODEL=${prov.OLLAMA_MODEL}`,
``,
`DEEPSEEK_ENABLED=${prov.DEEPSEEK_ENABLED}`,
`DEEPSEEK_API_KEY=${prov.DEEPSEEK_API_KEY}`,
`DEEPSEEK_MODEL=${prov.DEEPSEEK_MODEL}`,
`DEEPSEEK_BASE_URL=${prov.DEEPSEEK_BASE_URL}`,
``,
`MINIMAX_ENABLED=${prov.MINIMAX_ENABLED}`,
`MINIMAX_API_KEY=${prov.MINIMAX_API_KEY}`,
`MINIMAX_MODEL=${prov.MINIMAX_MODEL}`,
`MINIMAX_BASE_URL=${prov.MINIMAX_BASE_URL}`,
``,
`DEEPL_ENABLED=${prov.DEEPL_ENABLED}`,
`DEEPL_API_KEY=${prov.DEEPL_API_KEY}`,
``,
`OPENAI_ENABLED=${prov.OPENAI_ENABLED}`,
`OPENAI_API_KEY=${prov.OPENAI_API_KEY}`,
`OPENAI_MODEL=${prov.OPENAI_MODEL}`,
`OPENAI_BASE_URL=${prov.OPENAI_BASE_URL}`,
``,
`OPENROUTER_ENABLED=${prov.OPENROUTER_ENABLED}`,
`OPENROUTER_API_KEY=${prov.OPENROUTER_API_KEY}`,
`OPENROUTER_MODEL=${prov.OPENROUTER_MODEL}`,
``,
`# ---- Upload ----`,
`MAX_FILE_SIZE_MB=50`,
`ALLOWED_EXTENSIONS=.docx,.xlsx,.pptx`,
``,
`# ---- Rate Limiting ----`,
`RATE_LIMIT_ENABLED=true`,
`RATE_LIMIT_REQUESTS_PER_MINUTE=60`,
`RATE_LIMIT_TRANSLATIONS_PER_MINUTE=10`,
`RATE_LIMIT_TRANSLATIONS_PER_HOUR=100`,
`RATE_LIMIT_TRANSLATIONS_PER_DAY=500`,
``,
`# ---- Admin ----`,
`ADMIN_USERNAME=${adminUser}`,
`ADMIN_PASSWORD=${adminPass}`,
adminHash ? `ADMIN_PASSWORD_HASH=${adminHash}` : `# ADMIN_PASSWORD_HASH= (mot de passe en clair dans ADMIN_PASSWORD)`,
`JWT_SECRET_KEY=${jwtSecret}`,
`ADMIN_TOKEN_SECRET=${adminTokenSecret}`,
`CORS_ORIGINS=https://${domain}`,
``,
`# ---- Database ----`,
`POSTGRES_USER=translate`,
`POSTGRES_PASSWORD=${pgPassword}`,
`POSTGRES_DB=translate_db`,
``,
`# ---- Monitoring ----`,
`GRAFANA_USER=admin`,
`GRAFANA_PASSWORD=${grafanaPassword}`,
``,
`# ---- Stripe ----`,
`STRIPE_SECRET_KEY=${stripe.SECRET_KEY}`,
`STRIPE_WEBHOOK_SECRET=${stripe.WEBHOOK_SECRET}`,
`STRIPE_STARTER_PRICE_ID=${stripe.STARTER_PRICE_ID}`,
`STRIPE_PRO_PRICE_ID=${stripe.PRO_PRICE_ID}`,
`STRIPE_BUSINESS_PRICE_ID=${stripe.BUSINESS_PRICE_ID}`,
``,
];
writeEnv(lines.join('\n'));
console.log(`${C.GREEN}.env ecrit!${C.NC}`);
// ===== Offer to start =====
console.log(`\n${C.BOLD}Prochaines etapes :${C.NC}`);
console.log(` 1. Verifier: cat .env`);
console.log(` 2. Lancer: docker compose up -d --build`);
console.log(` 3. Tester: curl http://localhost:8000/health`);
console.log(``);
const doStart = await question('Lancer docker compose maintenant ? (oui/non)', 'non');
if (doStart === 'oui') {
console.log(`${C.CYAN}Build et lancement...${C.NC}`);
try {
execSync('docker compose up -d --build', { stdio: 'inherit', cwd: path.join(__dirname, '..'), timeout: 600000 });
console.log(`${C.GREEN}Demarre!${C.NC}`);
console.log(`\nVerification...`);
try {
const health = execSync('curl -sf http://localhost:8000/health', { encoding: 'utf-8', timeout: 10000 });
console.log(` Health: ${health}`);
} catch {
console.log(` ${C.YELLOW}Health check pas encore pret, attends 30s puis: curl http://localhost:8000/health${C.NC}`);
}
execSync('docker compose ps', { stdio: 'inherit', cwd: path.join(__dirname, '..') });
} catch (e) {
console.log(`${C.RED}Erreur docker. Verifie les logs: docker compose logs${C.NC}`);
}
}
console.log(`\n${C.GREEN}${C.BOLD}Setup termine!${C.NC}`);
console.log(`Admin: ${adminUser} / ${adminPass}`);
console.log(`Grafana: admin / ${grafanaPassword}`);
}
main().catch(e => {
console.error(`\nErreur: ${e.message}`);
process.exit(1);
});