278 lines
9.6 KiB
JavaScript
Executable File
278 lines
9.6 KiB
JavaScript
Executable File
require("dotenv").config();
|
|
const fs = require("fs-extra");
|
|
const path = require("path");
|
|
const express = require("express");
|
|
const cors = require("cors");
|
|
const cookieParser = require("cookie-parser");
|
|
const bodyParser = require("body-parser");
|
|
const glob = require("glob");
|
|
const Odmdb = require("./models/Odmdb.js");
|
|
const Caddy = require("./models/Caddy.js");
|
|
const logger = require("./utils/logger.js");
|
|
|
|
/**
|
|
* Main application class for the apxtri server.
|
|
* Encapsulates server setup and startup logic.
|
|
*/
|
|
class ApxtriApp {
|
|
constructor() {
|
|
this.dataPath = `${process.env.DATAPATH}/data`;
|
|
this.nodePath = `${process.env.NODEPATH}/${process.env.TOWN}-${process.env.NATION}`;
|
|
}
|
|
|
|
/**
|
|
* Starts the application server.
|
|
*/
|
|
async start() {
|
|
logger.info("Server starting...");
|
|
this._performPreflightChecks();
|
|
|
|
const conflist = this._discoverConfiguration();
|
|
if (process.env.MODE==="dev") {
|
|
await this._setupSharedSymlinks(conflist); // Now runs after and receives the config
|
|
}
|
|
await this._rebuildIndexes(conflist);
|
|
const app = this._setupExpress(conflist);
|
|
await this._configureCaddy(conflist);
|
|
|
|
this._listen(app);
|
|
}
|
|
|
|
/**
|
|
* Performs essential checks before starting the server.
|
|
* @private
|
|
*/
|
|
_performPreflightChecks() {
|
|
if (!fs.existsSync(this.dataPath)) {
|
|
logger.error(`FATAL: Data path not found at ${this.dataPath}. Check your .env configuration.`);
|
|
process.exit(1);
|
|
}
|
|
if (!fs.existsSync("/etc/caddy")) {
|
|
logger.error("FATAL: Caddy is not installed or /etc/caddy is not accessible.");
|
|
process.exit(1);
|
|
}
|
|
logger.info("Preflight checks passed.");
|
|
}
|
|
|
|
/**
|
|
* Scans the filesystem to discover all tribes, objects, and routes.
|
|
* @private
|
|
* @returns {object} The discovered configuration list.
|
|
*/
|
|
_discoverConfiguration() {
|
|
logger.info("Discovering configuration from filesystem...");
|
|
const conflist = { tribes: {}, caddy: [], corslist: [], apiroutes: [] };
|
|
glob.sync(`${this.dataPath}/*`).forEach(tribePath => {
|
|
const tribe = path.basename(tribePath);
|
|
conflist.tribes[tribe] = [];
|
|
|
|
glob.sync(`${tribePath}/objects/*`).forEach(objPath => {
|
|
const objname = path.basename(objPath);
|
|
if (!objname.includes('.')) {
|
|
conflist.tribes[tribe].push(objname);
|
|
}
|
|
});
|
|
// Special handling for 'apxtri' tribe to find web apps for Caddy
|
|
if (tribe === 'apxtri') {
|
|
glob.sync(`${tribePath}/objects/wwws/itm/*.json`).forEach(appItmPath => {
|
|
const appname = path.basename(appItmPath, '.json');
|
|
const appConf = fs.readJsonSync(appItmPath);
|
|
const caddy = {
|
|
dns: process.env.MODE === "dev" ? [`${appname}.${tribe}.${process.env.TOWN}.${process.env.NATION}`] : appConf.dns,
|
|
tribe,
|
|
appname
|
|
};
|
|
conflist.caddy.push(caddy);
|
|
caddy.dns.forEach(d => {
|
|
if (!conflist.corslist.includes(d)) conflist.corslist.push(d);
|
|
});
|
|
});
|
|
}
|
|
|
|
const routes = glob.sync(`${this.nodePath}/${tribe}/routes/*.js`).map(f => ({
|
|
url: `/${tribe}/${path.basename(f, ".js")}`,
|
|
route: f,
|
|
}));
|
|
conflist.apiroutes.push(...routes);
|
|
});
|
|
|
|
logger.info(`Discovered ${Object.keys(conflist.tribes).length} tribes.`);
|
|
return conflist;
|
|
}
|
|
|
|
/**
|
|
* Rebuilds all object indexes on startup and logs the time for each.
|
|
* It will log a warning in yellow if an index operation takes longer than 500ms.
|
|
* @private
|
|
*/
|
|
async _rebuildIndexes(conflist) {
|
|
logger.info("Rebuilding all object indexes...");
|
|
for (const tribe in conflist.tribes) {
|
|
for (const objname of conflist.tribes[tribe]) {
|
|
const indexIdentifier = `${tribe}/${objname}`;
|
|
try {
|
|
const startTime = Date.now();
|
|
|
|
const db = new Odmdb(objname, tribe);
|
|
await db.rebuildIndex();
|
|
|
|
const duration = Date.now() - startTime;
|
|
const logMessage = `- Indexing for ${indexIdentifier} complete in ${duration}ms.`;
|
|
|
|
// If the duration exceeds the threshold, log it as a warning (yellow).
|
|
if (duration > 500) {
|
|
logger.warn(logMessage);
|
|
} else {
|
|
logger.debug(logMessage);
|
|
}
|
|
|
|
// Give the event loop a chance to process logs from other modules
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
} catch (error) {
|
|
logger.error(`Failed to rebuild index for ${indexIdentifier}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
logger.info("Index rebuild complete.");
|
|
}
|
|
|
|
/**
|
|
* Configures and returns the Express application instance.
|
|
* @private
|
|
*/
|
|
_setupExpress(conflist) {
|
|
logger.info("Setting up Express server...");
|
|
const app = express();
|
|
const globalconfPath=path.join(this.dataPath, 'apxtri', 'objects', 'tribes', 'itm', 'apxtri.json')
|
|
let globalConf={
|
|
tribeId: "apxtri",
|
|
api: {
|
|
languages: ["en","fr"],
|
|
appset: {"trust proxy": true},
|
|
json: {
|
|
limit: "10mb",type: "application/json",rawBody: true},
|
|
bodyparse: {
|
|
urlencoded: {limit: "50mb",extended: true},
|
|
json: {limit: "500mb"}}
|
|
}, emailcontact: "",smtp: {},sms: {}}
|
|
if (!fs.existsSync(globalconfPath)){
|
|
fs.outputJSONSync(globalconfPath,globalConf,{spaces:2})
|
|
}
|
|
globalConf = fs.readJsonSync(globalconfPath);
|
|
|
|
app.disable("x-powered-by");
|
|
app.use(cookieParser());
|
|
app.use(bodyParser.json(globalConf.api.bodyparse.json));
|
|
app.use(bodyParser.urlencoded(globalConf.api.bodyparse.urlencoded));
|
|
|
|
// CORS configuration
|
|
const allowedOrigins = new RegExp(`(${conflist.corslist.map(d => d.replace(/\./g, "\\.")).join('|')})$`);
|
|
app.use(cors({
|
|
origin: (origin, callback) => {
|
|
if (!origin || allowedOrigins.test(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error(`CORS policy does not allow access from ${origin}`));
|
|
}
|
|
},
|
|
credentials: true,
|
|
}));
|
|
|
|
// Dynamically load all routes
|
|
logger.info("Loading API routes...");
|
|
conflist.apiroutes.forEach(r => {
|
|
try {
|
|
app.use(r.url, require(r.route));
|
|
logger.debug(`- Route loaded: ${r.url}`);
|
|
} catch (err) {
|
|
logger.error(`Failed to load route ${r.url} from ${r.route}: ${err.message}`);
|
|
}
|
|
});
|
|
|
|
app.locals.tribeids = Object.keys(conflist.tribes);
|
|
return app;
|
|
}
|
|
|
|
/**
|
|
* Configures and reloads Caddy.
|
|
* @private
|
|
*/
|
|
async _configureCaddy(conflist) {
|
|
logger.info("Configuring Caddy...");
|
|
const caddyConfResult = Caddy.getcaddyconf(conflist.caddy);
|
|
if (caddyConfResult.status === 200) {
|
|
await Caddy.reload(caddyConfResult.data.caddyconf);
|
|
logger.info("Caddy reloaded successfully.");
|
|
} else {
|
|
logger.error("Failed to generate Caddy configuration.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Usefull to do vibe codding in dev with gemini that have a limited files access
|
|
* it is not possioble to give gemini access to all data (use too much memory context)
|
|
* Ensures that `tmp/data` contains a mirrored directory for each tribe,
|
|
* with symlinks to the 'wco' and 'wwws' object directories inside an 'objects' folder.
|
|
* This version is idempotent and does not clean the directory, making it faster.
|
|
* @private
|
|
* @param {object} conflist - The configuration object from _discoverConfiguration.
|
|
*/
|
|
async _setupSharedSymlinks(conflist) {
|
|
logger.info("Ensuring shared symlinks for wco and wwws are up to date...");
|
|
const tmpDataPath = path.join(__dirname, 'tmp', 'data');
|
|
const tribeNames = Object.keys(conflist.tribes);
|
|
|
|
try {
|
|
for (const tribeName of tribeNames) {
|
|
const tribeSourcePath = path.join(this.dataPath, tribeName);
|
|
// The destination path is now the 'objects' directory for the tribe.
|
|
const tribeDestObjectsPath = path.join(tmpDataPath, tribeName, 'objects');
|
|
|
|
// 1. Ensure the destination 'objects' directory for the tribe exists.
|
|
// fs.ensureDir will create all necessary parent directories.
|
|
await fs.ensureDir(tribeDestObjectsPath);
|
|
|
|
// 2. Define the specific sub-directories to link.
|
|
const objectsToLink = ['wco', 'wwws'];
|
|
for (const objName of objectsToLink) {
|
|
const sourceObjectPath = path.join(tribeSourcePath, 'objects', objName);
|
|
// The link will be created inside the 'objects' directory.
|
|
const destLinkPath = path.join(tribeDestObjectsPath, objName);
|
|
|
|
// 3. Check if the source directory exists.
|
|
if (await fs.pathExists(sourceObjectPath)) {
|
|
// 4. ensureSymlink is idempotent.
|
|
await fs.ensureSymlink(sourceObjectPath, destLinkPath);
|
|
logger.debug(`Symlink ensured: ${destLinkPath}`);
|
|
}
|
|
}
|
|
}
|
|
logger.info("Symlink check complete.");
|
|
} catch (error) {
|
|
logger.error("Failed during symlink setup:", error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the Express server listener.
|
|
* @private
|
|
*/
|
|
async _listen(app) {
|
|
const ips = await Caddy.getip();
|
|
app.listen(process.env.APIPORT, () => {
|
|
logger.info(`Server listening on port ${process.env.APIPORT}`);
|
|
const adminUrl = `http(s)://admin.apxtri.${process.env.TOWN}.${process.env.NATION}`;
|
|
// Using console.log directly for final user-facing message
|
|
console.log(`\x1b[32m[apxtri] Server is ready. Admin UI might be available at: ${adminUrl} on ip:${JSON.stringify(ips)} :\x1b[0m`);
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- Start the application ---
|
|
const app = new ApxtriApp();
|
|
app.start().catch(error => {
|
|
logger.error("A critical error occurred during startup:", error);
|
|
process.exit(1);
|
|
}); |