const input = require("input") const fs = require("fs") const path = require("path") const { Umzug, SequelizeStorage } = require("umzug") const { Sequelize } = require("sequelize") const argon2 = require("argon2") const axios = require("axios") const os = require("os") const { execSync } = require("child_process") console.log("Troplo/Colubrina CLI") if (fs.existsSync("../backend/config/config.json")) { console.log( "Want to modify either the Colubrina, or database config? Check out the config files in backend/config." ) } console.log("Colubrina version", require("../frontend/package.json").version) async function checkForUpdates() { if (!process.argv.includes("--skip-update")) { await axios .get("https://services.troplo.com/api/v1/state", { headers: { "X-Troplo-Project": "colubrina", "X-Troplo-Project-Version": require("../frontend/package.json") .version }, timeout: 1000 }) .then((res) => { if (res.data.warning) { console.log(res.data.warning) } if ( require("../frontend/package.json").version !== res.data.latestVersion ) { console.log("A new version of Colubrina is available!") console.log("Latest version:", res.data.latestVersion) } else { console.log("Colubrina is up to date.") } }) .catch((e) => { console.log(e) console.log( "Failed to check for updates, ensure you are connected to the internet, and services.troplo.com is whitelisted behind any potential firewalls." ) }) } else { console.log("Skipping update check") } } let state = { db: { host: "localhost", port: 3306, username: "colubrina", password: null, database: "colubrina", storage: "../backend/storage.db", dialect: "mariadb" }, dbConfig: {} } async function doSetupDB() { const dialect = await input.select( "What database dialect do you want to use? (MariaDB tested, recommended)", ["mariadb", "postgres", "sqlite"] ) const host = await input.text("What is the host?", { default: state.db.host || "localhost" }) const port = await input.text("What is the port?", { default: state.db.port || 3306 }) const username = await input.text("What is the username?", { default: state.db.username || "colubrina" }) const password = await input.text("What is the password?", { default: state.db.password ? "Enter for cached password" : "Please specify" }) const database = await input.text("What is the database name?", { default: state.db.database || "colubrina" }) let storage if (dialect === "sqlite") { storage = await input.text( "What is the path to the storage file (SQLite only)?", { default: state.db.storage || "./storage.db" } ) } state.db = { username: username, password: password, database: database, host: host, dialect: dialect, port: port, logging: false } state.dbConfig = { development: { username: username, password: password, database: database, host: host, dialect: dialect, port: port, storage: dialect === "sqlite" ? storage : null, logging: false }, test: { username: username, password: password, database: database, host: host, dialect: dialect, port: port, logging: false }, production: { username: username, password: password, database: database, host: host, dialect: dialect, port: port, logging: false } } await testDB() } async function testDB() { try { const sequelize = new Sequelize(state.db) await sequelize.authenticate() console.log("Connection to database has been established successfully.") } catch (error) { console.error("Unable to connect to the database:", error) await doSetupDB() } } async function dbSetup() { await doSetupDB() fs.writeFileSync( path.join(__dirname, "../backend/config/database.json"), JSON.stringify(state.dbConfig, null, 2) ) console.log("config/database.json overwritten") } async function runMigrations() { console.log("Running migrations") execSync("cd ../backend && sequelize db:migrate", () => { console.log("Migrations applied") }) } async function createUser() { const user = { username: await input.text("Username", { default: "admin" }), password: await argon2.hash(await input.text("Password", {})), email: await input.text("Email", { default: "troplo@troplo.com" }), admin: await input.confirm("Admin (true/false)", { default: false }), emailVerified: await input.confirm("Email verified (true/false)", { default: true }) } const { User } = require("../backend/models") await User.create(user) console.log("User created") } async function configureDotEnv() { if (!fs.existsSync("../backend/config/config.json")) { fs.writeFileSync("../backend/config/config.json", "{}") } let config = require("../backend/config/config.json") config.hostname = await input.text("Public Domain", { default: "localhost" }) config.corsHostname = await input.text("CORS Hostname", { default: "http://localhost" }) config.siteName = await input.text("Site Name", { default: "Colubrina" }) config.allowRegistrations = await input.text("Allow Registrations", { default: true }) config.publicUsers = await input.text("Show instance users publicly?", { default: true }) config.emailVerify = await input.text("Enforce email verification?", { default: false }) if (config.emailVerify) { config.emailSMTPHost = await input.text("SMTP Host", { default: "mail.example.com" }) config.emailSMTPPort = await input.text("SMTP Port", { default: 587 }) config.emailSMTPUsername = await input.text("SMTP Username", { default: "colubrina@example.com" }) config.emailSMTPPassword = await input.text("SMTP Password", {}) config.emailSMTPFrom = await input.text("SMTP From Address", { default: "colubrina@example.com" }) config.emailSMTPSecure = await input.text("SMTP Secure", { default: true }) } else { config.emailSMTPHost = "smtp.myhost.com" config.emailSMTPPort = 587 config.emailSMTPUsername = "colubrina@example.com" config.emailSMTPFrom = "colubrina@example.com" config.emailSMTPPassword = "" config.emailSMTPSecure = true } config.notification = "" config.notificationType = "info" config.release = "stable" config.rules = "Write your instance rules here." fs.writeFileSync( "../backend/config/config.json", JSON.stringify(config, null, 2) ) } async function init() { while (true) { const option = await input.select(`Please select an option`, [ "First-time setup", "Create user", "Run migrations", "Update/create database config file", "Check for updates", "Build frontend for production", "Exit" ]) if (option === "First-time setup") { // run yarn install in ../backend console.log("Running yarn install") execSync("cd ../backend && yarn install --frozen-lockfile", () => { console.log("yarn install complete (backend)") }) execSync("cd ../frontend && yarn install --frozen-lockfile", () => { console.log("yarn install complete (frontend)") }) if ( fs.existsSync(path.join(__dirname, "../backend/config/config.json")) ) { const option = await input.confirm( "Colubrina config already exists, overwrite?", { default: false } ) if (option) { await configureDotEnv() } } else { await configureDotEnv() } if ( fs.existsSync(path.join(__dirname, "../backend/config/database.json")) ) { const option = await input.select( `config/database.json already exists. Do you want to overwrite it?`, ["Yes", "No"] ) if (option === "Yes") { await dbSetup() } } else { await dbSetup() } await runMigrations() const { User, Theme } = require("../backend/models") try { await Theme.bulkCreate( JSON.parse( fs.readFileSync(path.join(__dirname, "./templates/themes.json")) ) ) } catch { console.log("Themes already exist.") } try { await User.create({ username: "Colubrina", id: 0, bot: true, email: "colubrina@troplo.com", banned: true }) await User.update( { id: 0 }, { where: { username: "Colubrina" } } ) } catch { console.log("System user already exists.") } console.log("DB templates applied") console.log("Admin user creation") await createUser() console.log("Colubrina has been setup.") console.log( "Colubrina can be started with `yarn serve` or `node .` in the backend directory." ) console.log( "The Colubrina frontend can be built with `yarn build` in the root project directory, and is recommended to be served via NGINX, with a proxy_pass to the backend on /api and /socket.io." ) } else if (option === "Update/create database config file") { await dbSetup() console.log("config/database.json overwritten or created") } else if (option === "Create user") { await createUser() } else if (option === "Run migrations") { await runMigrations() } else if (option === "Check for updates") { await checkForUpdates() } else if (option === "Build frontend for production") { console.log("Building...") execSync( "cd ../frontend && yarn install --frozen-lockfile && yarn build", () => { console.log("yarn build complete") } ) } else if (option === "Exit") { process.exit(0) } } } checkForUpdates().finally(() => { init() })