const fs = require("fs-extra"); const path = require("path"); const Odmdb = require("./Odmdb.js"); const DATA_PATH = process.env.DATAPATH; const TRIBE_NAME = process.env.TOWN; const log = process.env.ACTIVELOG?.split(',').includes('Towns'); /** * Manages towns, which represent server instances in the network. */ class Towns { /** * Changes the owner (mayor) of a town. * @param {string} newOwnerAlias - The alias of the new owner. * @param {string} requestedByAlias - The alias of the user requesting the change (must be the current owner). * @returns {Promise} A response object. */ static async changeowner(newOwnerAlias, requestedByAlias) { try { const townsDb = new Odmdb('towns', TRIBE_NAME); const pagansDb = new Odmdb('pagans', TRIBE_NAME); const confPath = path.join(DATA_PATH, TRIBE_NAME, 'conf.json'); // 1. Verify the new owner exists as a pagan const newOwnerResult = await pagansDb.read(newOwnerAlias, { xprofils: ['admin'] }); if (newOwnerResult.status !== 200) { return { status: 404, ref: "Towns", msg: "new_owner_alias_not_found", data: { alias: newOwnerAlias } }; } // 2. Verify the requester is the current mayor const townConf = await fs.readJson(confPath); if (townConf.mayorId !== requestedByAlias) { return { status: 403, ref: "Towns", msg: "permission_denied_not_current_mayor" }; } // 3. Update the town's data file const townId = townConf.townId; const townUpdateData = { townId, mayorId: newOwnerAlias }; const role = { xprofils: ['admin', 'mayor'], xalias: requestedByAlias }; const updateResult = await townsDb.update( townUpdateData, role); if (updateResult.status !== 200) { return updateResult; // Forward the error from Odmdb } // 4. Update the local server configuration file townConf.mayorId = newOwnerAlias; await fs.outputJson(confPath, townConf, { spaces: 2 }); if (log) console.log(`[Towns] Town owner changed successfully to '${newOwnerAlias}' by '${requestedByAlias}'.`); return { status: 200, ref: "Towns", msg: "owner_change_successful", data: { alias: newOwnerAlias, townId } }; } catch (error) { console.error(`[Towns] Error changing owner to '${newOwnerAlias}':`, error); return { status: 500, ref: "Towns", msg: "internal_server_error", error: error.message }; } } /** * @deprecated The create method contains server setup logic (like nginx config) * and should not be part of a data model. This logic should be moved to a * dedicated setup or installation script. */ static create() { console.error("[Towns] The 'create' method is deprecated and should not be used. Please use a dedicated setup script."); return { status: 501, ref: "Towns", msg: "create_method_is_deprecated" }; } static async addCrontabEntry(cronExpression, command, identifier) { try { const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); const tmpCronFile = path.join(DATA_PATH, TRIBE_NAME, 'tmp', `cron_${Date.now()}.txt`); await fs.ensureDir(path.dirname(tmpCronFile)); const { stdout: currentCron } = await execAsync('crontab -l 2>/dev/null || true'); const cronLines = currentCron.split('\n').filter(line => { if (!line.trim()) return false; if (identifier && line.includes(identifier)) return false; return true; }); const cronEntry = `${cronExpression} ${command} # ${identifier}`; cronLines.push(cronEntry); await fs.writeFile(tmpCronFile, cronLines.join('\n') + '\n'); await execAsync(`crontab "${tmpCronFile}"`); await fs.remove(tmpCronFile); if (log) console.log(`[Towns] Crontab entry added: ${cronEntry}`); return { status: 200, ref: "Towns", msg: "crontab_entry_added", data: { cronEntry } }; } catch (error) { console.error('[Towns] Error adding crontab entry:', error); return { status: 500, ref: "Towns", msg: "crontab_entry_failed", error: error.message }; } } static async removeCrontabEntry(identifier) { try { const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); const tmpCronFile = path.join(DATA_PATH, TRIBE_NAME, 'tmp', `cron_${Date.now()}.txt`); await fs.ensureDir(path.dirname(tmpCronFile)); const { stdout: currentCron } = await execAsync('crontab -l 2>/dev/null || true'); const cronLines = currentCron.split('\n').filter(line => { if (!line.trim()) return false; if (identifier && line.includes(identifier)) return false; return true; }); await fs.writeFile(tmpCronFile, cronLines.join('\n') + '\n'); await execAsync(`crontab "${tmpCronFile}"`); await fs.remove(tmpCronFile); if (log) console.log(`[Towns] Crontab entries removed for: ${identifier}`); return { status: 200, ref: "Towns", msg: "crontab_entry_removed", data: { identifier } }; } catch (error) { console.error('[Towns] Error removing crontab entry:', error); return { status: 500, ref: "Towns", msg: "crontab_remove_failed", error: error.message }; } } } module.exports = Towns;