Files
apxtri/apxtri.js
2025-08-01 09:36:49 +02:00

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);
});