From e6e13b34947e3f0e231e11c3b819c868b9117522 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Thu, 8 Jan 2026 23:55:58 -0700 Subject: [PATCH] Move stuff; Add basic auth system; Move API endpoints --- .gitignore | 4 +- features.md | 11 --- index.js | 146 +++++++++++++++++++++++++------ package-lock.json | 195 +++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- routes/.js | 11 +++ routes/api/auth.js | 48 +++++++++++ routes/api/portal.js | 25 ++++++ routes/login.js | 11 +++ routes/logout.js | 20 +++++ usr/bin/ast_originate | 19 +++- views/index.ejs | 8 +- views/login.ejs | 51 ++++++++++- 13 files changed, 507 insertions(+), 46 deletions(-) create mode 100644 routes/.js create mode 100644 routes/api/auth.js create mode 100644 routes/api/portal.js create mode 100644 routes/login.js create mode 100644 routes/logout.js diff --git a/.gitignore b/.gitignore index 8dbe5bc..789dab7 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,6 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ -slingtoken.txt \ No newline at end of file +slingtoken.txt +sessions/ +auth.json \ No newline at end of file diff --git a/features.md b/features.md index e0a3cc8..e69de29 100644 --- a/features.md +++ b/features.md @@ -1,11 +0,0 @@ -Default Roles -- SuperAdmin (Do all the thing) -- Admin (Can't edit superadmins) -- User (Normal user stuff (No user editing lol)) - -Individual Perms (For custom roles?) -- Can Page -- View Users -- Create Users -- Delete Users -- \ No newline at end of file diff --git a/index.js b/index.js index 8b80f3f..2944898 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ const phonesCfg = require('./phones.json'); const buttonsCfg = require('./buttons.json'); const fs = require('fs'); const path = require('path'); +const bcrypt = require('bcrypt'); if (!fs.existsSync(path.join(__dirname, 'slingtoken.txt'))) { // Create empty file @@ -19,20 +20,19 @@ const contexts = {}; // Generate contexts from buttonsCfg Object.keys(buttonsCfg).forEach(category => { - buttonsCfg[category].forEach(button => { - if (button.name && button.context) { - contexts[button.name] = { - context: button.context.context, - timeout: button.context.timeout, - cid: button.context.cid, - ...(button.context.dial && { dial: button.context.dial }), + buttonsCfg[category].forEach(button => { + if (button.name && button.context) { + contexts[button.name] = { + context: button.context.context, + timeout: button.context.timeout, + cid: button.context.cid, + ...(button.context.dial && { dial: button.context.dial }), ...(button.sling_chat_id && { sling_chat_id: button.sling_chat_id }), ...(button.sling_chat_message && { sling_chat_message: button.sling_chat_message }) - }; - } - }); + }; + } + }); }); - //console.log('Generated contexts:', contexts); function trigCall(pageType, phone) { @@ -77,7 +77,7 @@ function trigCall(pageType, phone) { function originateCall(number, context, delay, timeout, cid, variables = {}) { // Build the base command let command = `/usr/bin/ast_originate ${number} ${context} ${delay} ${timeout} ${Buffer.from(cid).toString('base64')}`; - + // Add variables if provided if (variables && typeof variables === 'object') { const varString = Object.entries(variables) @@ -87,7 +87,7 @@ function originateCall(number, context, delay, timeout, cid, variables = {}) { command += ` ${varString}`; } } - + return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { @@ -104,7 +104,7 @@ function originateCall(number, context, delay, timeout, cid, variables = {}) { async function slingAuthLoop() { console.log("Start slingAuthLoop") if (!slingToken) { - console.warn('No Sling token provided in environment variables.'); + console.warn('No Sling token provided.'); return; } setTimeout(slingAuthLoop, 15 * 60 * 1000); // Refresh every 15 minutes @@ -136,36 +136,102 @@ async function slingAuthLoop() { slingAuthLoop(); +global.contexts = contexts; +global.trigCall = trigCall; +global.buttonsCfg = buttonsCfg; +global.phonesCfg = phonesCfg; +global.slingToken = slingToken; +global.originateCall = originateCall; +global.exec = exec; + const app = express(); const HOST = process.env.HOST || 'localhost'; const PORT = process.env.PORT || 3000; app.use(express.json()); +app.use(express.urlencoded({ extended: true })); app.set('view engine', 'ejs'); app.set('views', './views'); app.use(express.static('static')); -app.use(express.urlencoded({ extended: true })); app.use(require('express-session')({ secret: process.env.SESSION_SECRET || 'fallback-secret-key', resave: false, saveUninitialized: false, - cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } + cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 }, + store: new (require('session-file-store')(require('express-session')))({ + path: path.join(__dirname, 'sessions'), + ttl: 24 * 60 * 60, // 1 day + retries: 1, + reapInterval: 60 * 60 // prune expired sessions every hour + }), })); function auth(req, res, next) { if (!req.session || !req.session.authenticated) { return res.redirect('/login'); } + if (req.session.user && req.session.user.remember) { + // Extend session expiration + req.session.cookie.maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days + } next(); } -app.get('/', (req, res) => { - res.render('index', { session: req.session, phones: phonesCfg, buttons: require("./buttons.json") }); -}); +function apiAuth(req, res, next) { + console.log(req.headers) + if (req.headers["authorization"]) { + if (req.headers["authorization"] === process.env.API_TOKEN) { + return next(); + } else { + console.log('Unauthorized API access attempt with invalid token'); + return res.status(401).json({ success: false, message: 'Unauthorized' }); + } + } + if (!req.session || !req.session.authenticated) { + console.log('Unauthorized API access attempt without valid session'); + return res.status(401).json({ success: false, message: 'Unauthorized' }); + } + if (req.session.user && req.session.user.remember) { + // Extend session expiration + req.session.cookie.maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days + } + next(); +} + +global.auth = auth; +global.apiAuth = apiAuth; + +// folder where routes live +const ROUTES_DIR = path.join(__dirname, "routes"); + +// Recursive route loader +function loadRoutes(dir, baseRoute = "") { + fs.readdirSync(dir).forEach((file) => { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + // If folder, recurse deeper + const nestedBase = baseRoute + "/" + file; + loadRoutes(fullPath, nestedBase); + } else if (file.endsWith(".js")) { + // If .js file → mount route + const routePath = + baseRoute + + "/" + + file.replace(".js", "").replace("index", ""); + + const router = require(fullPath); + + console.log(`Mounting route: ${routePath || "/"}`); + app.use(routePath || "/", router); + } + }); +} + +// Load everything in routes/ +loadRoutes(ROUTES_DIR); -app.get('/login', (req, res) => { - res.render('login', { session: req.session }); -}); app.post('/trig', async (req, res) => { console.log('Triggering call with data:', req.body); @@ -176,10 +242,10 @@ app.post('/trig', async (req, res) => { app.post('/stop', async (req, res) => { console.log('Stopping page for phone:', req.body.phone); // Logic to stop the page would go here. - // For now we will just log it, as the specific asterisk command to hangup a channel depends on implementation details not provided. - // Typically it might involve 'asterisk -rx "channel request hangup "' or similar via AMI. - // Assuming we might want to run a command similar to originate but for hangup if we knew the channel. - // Since we don't have the channel ID easily available without tracking it, we might need to implement channel tracking or use a broad command. + // For now we will just log it, as the specific asterisk command to hangup a channel depends on implementation details not provided. + // Typically it might involve 'asterisk -rx "channel request hangup "' or similar via AMI. + // Assuming we might want to run a command similar to originate but for hangup if we knew the channel. + // Since we don't have the channel ID easily available without tracking it, we might need to implement channel tracking or use a broad command. exec(`/usr/bin/ast_drop ${process.env.PAGE_GROUP || '9000'}`, (error, stdout, stderr) => { if (error) { console.error(`Error stopping page: ${error}`); @@ -187,10 +253,36 @@ app.post('/stop', async (req, res) => { } console.log(`Page stopped: ${stdout}`); }); - + res.status(200).send('Stop request received'); }); +const convertPasswords = () => { // Read auth.json and convert plaintext to hashed passwords + const authFilePath = path.join(__dirname, 'auth.json'); + if (fs.existsSync(authFilePath)) { + let authData = JSON.parse(fs.readFileSync(authFilePath, 'utf8')); + let updated = false; + authData = authData.map(user => { + if (user.password) { + const hashedPassword = bcrypt.hashSync(user.password, 10); + updated = true; + return { ...user, passwordHash: hashedPassword, password: undefined }; + } + return user; + }); + if (updated) { + fs.writeFileSync(authFilePath, JSON.stringify(authData, null, 4), 'utf8'); + console.log('Converted plaintext passwords to hashed passwords in auth.json'); + } + } else { + console.warn('auth.json file not found for password conversion.'); + } + + global.authUsers = JSON.parse(fs.readFileSync(authFilePath, 'utf8')); +}; + +convertPasswords(); + app.listen(PORT, HOST, () => { console.log(`Server running on http://${HOST}:${PORT}`); }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0e56c3f..695c044 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "ejs": "^3.1.10", "express": "^5.2.1", - "express-session": "^1.18.2" + "express-session": "^1.18.2", + "session-file-store": "^1.5.0" } }, "node_modules/accepts": { @@ -28,18 +30,56 @@ "node": ">= 0.6" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/bagpipe": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz", + "integrity": "sha512-42sAlmPDKes1nLm/aly+0VdaopSU9br+jkRELedhQxI5uXHgtk47I83Mpmf4zoNTRMASdLFtUkimlu/Z9zQ8+g==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", @@ -409,6 +449,20 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -467,6 +521,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -527,6 +587,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -548,6 +617,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -565,6 +640,27 @@ "node": ">=10" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kruptein": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-2.2.3.tgz", + "integrity": "sha512-BTwprBPTzkFT9oTugxKd3WnWrX630MqUDsnmBuoa98eQs12oD4n4TeI0GbpdGcYn/73Xueg2rfnw+oK4dovnJg==", + "license": "MIT", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">6" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -620,6 +716,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -647,6 +749,35 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -775,6 +906,15 @@ "node": ">= 0.10" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -862,6 +1002,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/session-file-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/session-file-store/-/session-file-store-1.5.0.tgz", + "integrity": "sha512-60IZaJNzyu2tIeHutkYE8RiXVx3KRvacOxfLr2Mj92SIsRIroDsH0IlUUR6fJAjoTW4RQISbaOApa2IZpIwFdQ==", + "license": "Apache-2.0", + "dependencies": { + "bagpipe": "^0.3.5", + "fs-extra": "^8.0.1", + "kruptein": "^2.0.4", + "object-assign": "^4.1.1", + "retry": "^0.12.0", + "write-file-atomic": "3.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -940,6 +1097,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -972,6 +1135,15 @@ "node": ">= 0.6" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -984,6 +1156,15 @@ "node": ">= 0.8" } }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1007,6 +1188,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } } } } diff --git a/package.json b/package.json index 9f4b4cd..7487a20 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "license": "ISC", "type": "commonjs", "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "ejs": "^3.1.10", "express": "^5.2.1", - "express-session": "^1.18.2" + "express-session": "^1.18.2", + "session-file-store": "^1.5.0" } } diff --git a/routes/.js b/routes/.js new file mode 100644 index 0000000..655bcbe --- /dev/null +++ b/routes/.js @@ -0,0 +1,11 @@ +// routes/users.js +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs"); + +router.get('/', global.auth, (req, res) => { + res.render('index', { session: req.session, phones: global.phonesCfg, buttons: global.buttonsCfg }); +}); + +module.exports = router; diff --git a/routes/api/auth.js b/routes/api/auth.js new file mode 100644 index 0000000..c76f2b8 --- /dev/null +++ b/routes/api/auth.js @@ -0,0 +1,48 @@ +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs"); +const bcrypt = require('bcrypt'); + + +router.get("/session", (req, res) => { + if (req.session && req.session.authenticated) { + res.status(200).json({ authenticated: true, user: req.session.user, sessionID: req.sessionID }); + } else { + res.status(401).json({ authenticated: false }); + } +}); + +router.post("/login", (req, res) => { + const { username, password } = req.body; + // Locate user in global.authUsers [{username, password}]. password is bcrypted. + const user = global.authUsers.find(u => u.username === username); + if (user) { + // Compare password + bcrypt.compare(password, user.passwordHash, (err, result) => { + if (result) { + // Passwords match + req.session.authenticated = true; + req.session.user = { username: user.username, fullname: user.fullname, remember: req.body.remember || false }; + res.status(200).json({ success: true, message: 'Login successful' }); + } else { + // Passwords don't match + res.status(401).json({ success: false, message: 'Invalid credentials' }); + } + }); + } else { + // User not found + res.status(401).json({ success: false, message: 'Invalid credentials' }); + } +}); + +router.post("/logout", (req, res) => { + req.session.destroy(err => { + if (err) { + return res.status(500).json({ success: false, message: 'Logout failed' }); + } + res.status(200).json({ success: true, message: 'Logged out successfully' }); + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/api/portal.js b/routes/api/portal.js new file mode 100644 index 0000000..62bb8a0 --- /dev/null +++ b/routes/api/portal.js @@ -0,0 +1,25 @@ +// routes/users.js +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs"); + +router.post("/trigger", global.apiAuth, (req, res) => { + console.log('Triggering call with data:', req.body); + global.trigCall(req.body.pageType, req.body.phone); + res.status(200).send('Call triggered'); +}); + +router.post("/stop", global.apiAuth, (req, res) => { + console.log('Stopping all calls'); + global.exec(`/usr/bin/ast_drop ${process.env.PAGE_GROUP || '9000'}`, (error, stdout, stderr) => { + if (error) { + console.error(`Error stopping page: ${error}`); + return res.status(500).send('Error stopping page'); + } + console.log(`Page stopped: ${stdout}`); + }); + + res.status(200).send('Stop request received'); +}); +module.exports = router; diff --git a/routes/login.js b/routes/login.js new file mode 100644 index 0000000..85c948e --- /dev/null +++ b/routes/login.js @@ -0,0 +1,11 @@ +// routes/users.js +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs"); + +router.get('/', (req, res) => { + res.render('login'); +}); + +module.exports = router; diff --git a/routes/logout.js b/routes/logout.js new file mode 100644 index 0000000..746d582 --- /dev/null +++ b/routes/logout.js @@ -0,0 +1,20 @@ +// routes/users.js +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs"); + +router.get('/', (req, res) => { + if (req.session && req.session.authenticated) { + req.session.destroy(err => { + if (err) { + return res.redirect('/'); + } + res.redirect('/login'); + }) + } else { + res.redirect('/login'); + } +}); + +module.exports = router; diff --git a/usr/bin/ast_originate b/usr/bin/ast_originate index 8d55faa..0ee9d62 100644 --- a/usr/bin/ast_originate +++ b/usr/bin/ast_originate @@ -35,14 +35,29 @@ $callback_callerid = isset($argv[5]) ? base64_decode($argv[5]) : ""; */ $variable = []; -/*for ($i = 6; $i < $argc; $i++) { +for ($i = 6; $i < $argc; $i++) { if (strpos($argv[$i], '=') !== false) { list($k, $v) = explode('=', $argv[$i], 2); $variable[$k] = $v; } -}*/ +} +// Debug: echo main parameters +echo "DEBUG: callback_number={$callback_number}\n"; +echo "DEBUG: callback_destination={$callback_destination}\n"; +echo "DEBUG: pause_seconds={$pause_seconds}\n"; +echo "DEBUG: callback_timeout={$callback_timeout}\n"; +echo "DEBUG: callback_callerid={$callback_callerid}\n"; +// Echo parsed VAR=value channel variables +if (!empty($variable)) { + echo "DEBUG: channel variables:\n"; + foreach ($variable as $k => $v) { + echo " {$k}={$v}\n"; + } +} else { + echo "DEBUG: channel variables: (none)\n"; +} /* * AMI expects variables as: diff --git a/views/index.ejs b/views/index.ejs index 0102dc8..b3ba1e2 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -84,6 +84,10 @@ + + + Logout + @@ -150,7 +154,7 @@ dial: button.dataset.dial } }; - fetch('/trig', { + fetch('/api/portal/trigger', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) @@ -160,7 +164,7 @@ function stopPage() { const phoneSelect = document.getElementById('phoneSelect'); const phone = phoneSelect.value; - fetch('/stop', { + fetch('/api/portal/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); diff --git a/views/login.ejs b/views/login.ejs index 81c1ef9..17f00fa 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -33,7 +33,7 @@ <% } %> -
+
@@ -75,5 +75,54 @@
+ + \ No newline at end of file