UNPKG

4bnode

Version:

4bnode is a CLI-powered backend development platform with a built-in visual dashboard to generate, manage, and test Node.js/Express APIs faster.

154 lines (137 loc) 6.64 kB
#!/usr/bin/env node import fs from "fs"; import path from "path"; import os from "os"; import { createRequire } from "module"; import { pathToFileURL } from "url"; import { input, select, confirm, password } from "@inquirer/prompts"; import { getProjectRoot } from "./lib/project.js"; import { updateEnvFile } from "./lib/env.js"; import { showHeader, showTaskDone, showError, showInfo, showFileAction, installDeps } from "./lib/ui.js"; import { MAIL_PROVIDERS, buildMailerService, isOfficialMailDomain, OFFICIAL_MAIL_DOMAIN_PASSWORD, OFFICIAL_MAIL_NOTIFY, } from "./lib/codegen.js"; // The 4brains.in domain can't be configured directly — require the shared // authorization password (exits on a wrong answer). async function ensureOfficialAllowed(from) { if (!isOfficialMailDomain(from)) return; showInfo("4brains.in is an official email domain — you can't use it directly."); const pw = (await password({ message: "Authorization password for 4brains.in:", mask: "*" })).trim(); if (pw !== OFFICIAL_MAIL_DOMAIN_PASSWORD) { showError("Incorrect authorization password. 4brains.in was not configured."); process.exit(1); } } // Report an authorized 4brains.in configuration to the domain owner (best-effort; // uses the project's just-installed mail package). async function sendOfficialNotify(root, ctx) { const require = createRequire(path.join(root, "package.json")); let appName = "unknown"; try { appName = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).name || appName; } catch {} let osUser = "unknown"; try { osUser = os.userInfo().username; } catch {} const subject = "[4bnode] 4brains.in sender configured: " + ctx.from; const text = [ "A 4brains.in sender was configured in a 4bnode app (via CLI).", "", "Email id (from): " + ctx.from, "Provider: " + ctx.kind, "Configured by: " + osUser + "@" + os.hostname(), "App: " + appName, "Time: " + new Date().toISOString(), ].join("\n"); if (ctx.kind === "resend") { const { Resend } = await import(pathToFileURL(require.resolve("resend")).href); const resend = new Resend(ctx.apiKey); const { error } = await resend.emails.send({ from: ctx.from, to: OFFICIAL_MAIL_NOTIFY, subject, text }); if (error) throw new Error(error.message || "Resend error"); return; } const nodemailer = (await import(pathToFileURL(require.resolve("nodemailer")).href)).default; const transporter = nodemailer.createTransport({ host: ctx.smtp.host, port: Number(ctx.smtp.port) || 587, secure: ctx.smtp.secure, auth: { user: ctx.smtp.user, pass: ctx.smtp.pass }, }); await transporter.sendMail({ from: ctx.from, to: OFFICIAL_MAIL_NOTIFY, subject, text }); } async function main() { showHeader("add-email", "Set up transactional email"); const providerId = await select({ message: "Email provider:", choices: Object.entries(MAIL_PROVIDERS).map(([id, p]) => ({ name: p.label, value: id })), }); const preset = MAIL_PROVIDERS[providerId]; if (preset.hint) showInfo(preset.hint); const root = getProjectRoot(); const envPath = path.join(root, ".env"); const dir = path.join(root, "src", "services"); const mailerPath = path.join(dir, "mailer.js"); updateEnvFile(envPath, "MAIL_PROVIDER", providerId); const summary = [`Provider: ${preset.label}`]; let dep; let notifyCtx = null; // set when an authorized 4brains.in sender is configured if (preset.type === "resend") { // IntraApp(Postmaster) → Resend API. Only an API key + from address. const apiKey = (await password({ message: "Resend API key (re_...):", mask: "*" })).trim(); const from = (await input({ message: "From address:", default: "" })).trim(); await ensureOfficialAllowed(from); if (isOfficialMailDomain(from)) updateEnvFile(envPath, "OFFICIAL_MAIL_AUTHORIZED", "1"); if (apiKey) updateEnvFile(envPath, "RESEND_API_KEY", apiKey); if (from) updateEnvFile(envPath, "MAIL_FROM", from); dep = "resend"; summary.push(from ? `From: ${from}` : "From: (set MAIL_FROM in .env)"); if (isOfficialMailDomain(from) && apiKey) notifyCtx = { kind: "resend", from, apiKey }; } else { // Custom SMTP → nodemailer. TLS selects the port (465 vs 587). const host = (await input({ message: "SMTP host:", default: preset.host || "" })).trim(); const secure = await confirm({ message: "Use TLS (port 465)? (No = port 587)", default: false }); const defPort = secure ? 465 : 587; const port = (await input({ message: "SMTP port:", default: String(defPort) })).trim(); const user = (await input({ message: "SMTP user:", default: "" })).trim(); const pass = (await password({ message: "SMTP password:", mask: "*" })).trim(); const from = (await input({ message: "From address:", default: user })).trim(); await ensureOfficialAllowed(from || user); if (isOfficialMailDomain(from || user)) updateEnvFile(envPath, "OFFICIAL_MAIL_AUTHORIZED", "1"); updateEnvFile(envPath, "SMTP_HOST", host); updateEnvFile(envPath, "SMTP_PORT", port || String(defPort)); updateEnvFile(envPath, "SMTP_SECURE", secure ? "true" : "false"); updateEnvFile(envPath, "SMTP_USER", user); if (pass) updateEnvFile(envPath, "SMTP_PASS", pass); updateEnvFile(envPath, "SMTP_FROM", from || user); dep = "nodemailer"; summary.push(`Host: ${host}:${port || defPort}`); if (isOfficialMailDomain(from || user)) notifyCtx = { kind: "smtp", from: from || user, smtp: { host, port: port || defPort, secure, user, pass } }; } console.log(); showFileAction("updated", ".env"); fs.mkdirSync(dir, { recursive: true }); // Rewrite the mailer only if missing or on the old single-backend version. if (!fs.existsSync(mailerPath) || !fs.readFileSync(mailerPath, "utf8").includes("MAIL_PROVIDER")) { fs.writeFileSync(mailerPath, buildMailerService(), "utf8"); showFileAction(fs.existsSync(mailerPath) ? "updated" : "created", "src/services/mailer.js"); } else { showInfo("src/services/mailer.js already up to date — keeping it."); } await installDeps(dep); if (notifyCtx) { try { await sendOfficialNotify(root, notifyCtx); showInfo(`4brains.in configuration reported to ${OFFICIAL_MAIL_NOTIFY}.`); } catch (e) { showInfo(`Could not send the 4brains.in notification: ${e.message}`); } } summary.push("Use sendMail({ to, subject, html }) from src/services/mailer.js"); showTaskDone("Email configured", summary); } main().catch((err) => { if (err.name === "ExitPromptError") process.exit(0); showError(err.message); process.exit(1); });