867 lines
28 KiB
JavaScript
867 lines
28 KiB
JavaScript
/*
|
|
______ __ ______
|
|
/ \ / | / \
|
|
/$$$$$$ | _______ _$$ |_ ______ ______ /$$$$$$ | ______ _____ ____
|
|
$$ |__$$ | / |/ $$ | / \ / \ $$ | $$/ / \ / \/ \
|
|
$$ $$ |/$$$$$$$/ $$$$$$/ /$$$$$$ |/$$$$$$ |$$ | /$$$$$$ |$$$$$$ $$$$ |
|
|
$$$$$$$$ |$$ \ $$ | __ $$ | $$/ $$ | $$ |$$ | __ $$ | $$ |$$ | $$ | $$ |
|
|
$$ | $$ | $$$$$$ | $$ |/ |$$ | $$ \__$$ |$$ \__/ |$$ \__$$ |$$ | $$ | $$ |
|
|
$$ | $$ |/ $$/ $$ $$/ $$ | $$ $$/ $$ $$/ $$ $$/ $$ | $$ | $$ |
|
|
$$/ $$/ $$$$$$$/ $$$$/ $$/ $$$$$$/ $$$$$$/ $$$$$$/ $$/ $$/ $$/
|
|
|
|
Made with <3 by Chris Chrome and the rest of the AstroCom team
|
|
*/
|
|
|
|
require("dotenv").config();
|
|
const { execSync } = require('child_process');
|
|
const express = require('express');
|
|
const expressSession = require('express-session');
|
|
const ejs = require("ejs")
|
|
const mariadb = require('mariadb');
|
|
const bcrypt = require("bcrypt")
|
|
const crypto = require("crypto")
|
|
const app = express();
|
|
const port = process.env.SERVER_PORT || 3000;
|
|
|
|
const invalidBlocks = [
|
|
// Emergency number prefixes (112, 911, 999, 110, 117, 119, 113, 191, 111)
|
|
1120000, // UK, EU, etc
|
|
9110000, // US, Canada
|
|
9880000, // Suicide prevention (US)
|
|
9990000,
|
|
1100000,
|
|
1170000,
|
|
1190000,
|
|
1130000,
|
|
1910000,
|
|
1110000
|
|
]
|
|
|
|
const pool = mariadb.createPool({
|
|
host: process.env.DB_HOST || '127.0.0.1',
|
|
port: process.env.DB_PORT || 3306,
|
|
user: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASSWORD || '',
|
|
database: process.env.DB_NAME || 'astrocom',
|
|
connectionLimit: 10
|
|
});
|
|
const saltRounds = 10;
|
|
|
|
pool.getConnection().then((conn) => {
|
|
require("./migrations")(pool).then(() => {
|
|
conn.query("SELECT * FROM users WHERE id = 1").then((row) => {
|
|
if (!row || process.env.RESET_ADMIN == "true") {
|
|
// delete all users (The big scary one lol)
|
|
conn.query("DELETE FROM users").then(() => {
|
|
// Generate 32 char random string
|
|
const passwd = crypto.randomBytes(32).toString('hex');
|
|
bcrypt.hash(passwd, 10).then((hash) => {
|
|
conn.query("INSERT INTO users (id, username, passwordHash) VALUES (1, 'admin', ?)",
|
|
[hash]).then(() => {
|
|
console.log(`Created admin user with password: ${passwd}`);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
|
|
const gitCommitHashShort = execSync('git rev-parse --short HEAD').toString().trim();
|
|
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
const version = `${gitCommitHashShort}-${branch}`;
|
|
|
|
console.log(`Version: ${version}`);
|
|
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
app.use(expressSession({
|
|
store: expressSession.MemoryStore(),
|
|
secret: process.env.SESSION_SECRET || 'default_secret',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
secure: process.env.NODE_ENV === 'production',
|
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
|
}
|
|
}));
|
|
|
|
app.set('view engine', 'ejs');
|
|
app.set('views', __dirname + '/views');
|
|
// Static files
|
|
app.use(express.static('public'));
|
|
|
|
const addAnalytic = (tag) => {
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT * FROM analytics WHERE tag = ?", [tag]).then((rows) => {
|
|
if (rows.length === 0) {
|
|
conn.query("INSERT INTO analytics (tag, count) VALUES (?, 1)", [tag]).catch(err => {
|
|
console.error('Error creating analytics:', err);
|
|
});
|
|
} else {
|
|
conn.query("UPDATE analytics SET count = count + 1 WHERE tag = ?", [tag]).catch(err => {
|
|
console.error('Error updating analytics:', err);
|
|
});
|
|
}
|
|
}).catch(err => {
|
|
console.error('Error checking analytics:', err);
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
}).catch(err => {
|
|
console.error('Error getting connection:', err);
|
|
});
|
|
}
|
|
|
|
const dailyAnalytic = (tag) => { // This is a bit more complex, but it's just a daily count
|
|
// check if the tag, and tag_date exists. If the date is not today reset count to 0 and date to today
|
|
// If the date is today, increment count
|
|
const date = new Date();
|
|
const today = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT * FROM dailyAnalytics WHERE tag = ? AND tag_date = ?", [tag, today]).then((rows) => {
|
|
if (rows.length === 0) {
|
|
conn.query("INSERT INTO dailyAnalytics (tag, tag_date, count) VALUES (?, ?, 1)", [tag, today]).catch(err => {
|
|
console.error('Error creating daily analytics:', err);
|
|
});
|
|
} else {
|
|
conn.query("UPDATE dailyAnalytics SET count = count + 1 WHERE tag = ? AND tag_date = ?", [tag, today]).catch(err => {
|
|
console.error('Error updating daily analytics:', err);
|
|
});
|
|
}
|
|
}).catch(err => {
|
|
console.error('Error checking daily analytics:', err);
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
}).catch(err => {
|
|
console.error('Error getting connection:', err);
|
|
});
|
|
}
|
|
|
|
app.use((req, res, next) => {
|
|
if (req.path.startsWith("/api/v1")) {
|
|
addAnalytic("apiCalls");
|
|
};
|
|
next();
|
|
})
|
|
|
|
// Admin routes
|
|
|
|
// admin/logout
|
|
app.get('/admin/logout', (req, res) => {
|
|
req.session.destroy();
|
|
res.redirect('/admin/login');
|
|
});
|
|
|
|
app.get('/admin/login', (req, res) => {
|
|
res.render('admin/login');
|
|
});
|
|
|
|
app.get('/admin', (req, res) => {
|
|
if (!req.session.adminAuthenticated) {
|
|
res.redirect('/admin/login');
|
|
return;
|
|
}
|
|
res.render('admin/index', { user: req.session.user });
|
|
});
|
|
|
|
app.get('/admin/create', (req, res) => {
|
|
if (!req.session.adminAuthenticated) {
|
|
res.redirect('/admin/login');
|
|
return;
|
|
}
|
|
res.render('admin/create', { user: req.session.user });
|
|
});
|
|
|
|
app.get('/admin/route/:id', (req, res) => {
|
|
if (!req.session.adminAuthenticated) {
|
|
res.redirect('/admin/login');
|
|
return;
|
|
}
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM routes WHERE id = ?', [req.params.id]).then((rows) => {
|
|
const row = rows[0];
|
|
if (!row) {
|
|
res.status(404).send('Not Found');
|
|
return;
|
|
}
|
|
res.render('admin/edit', { user: req.session.user, data: row });
|
|
}).catch(err => {
|
|
console.error('Error getting route:', err);
|
|
res.status(500).send('Internal server error');
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.post('/admin/login', (req, res) => {
|
|
const username = req.body.username;
|
|
const password = req.body.password;
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT * FROM users WHERE username = ?", [String(username)]).then((rows) => {
|
|
const row = rows[0];
|
|
if (!row) {
|
|
res.status(401).send('Unauthorized (Not Found)');
|
|
return;
|
|
}
|
|
bcrypt.compare(password, row.passwordHash, (err, result) => {
|
|
if (err) {
|
|
console.error('Error comparing password:', err);
|
|
res.status(500).send('Internal server error');
|
|
return;
|
|
}
|
|
if (result) {
|
|
req.session.adminAuthenticated = true;
|
|
req.session.user = row.username;
|
|
res.redirect('/admin');
|
|
} else {
|
|
res.status(401).send('Unauthorized');
|
|
}
|
|
});
|
|
}).catch(err => {
|
|
console.error('Error getting user:', err);
|
|
res.status(500).send('Internal server error');
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
})
|
|
|
|
app.get('/api/v1/admin/routes', (req, res) => { // Get all routes
|
|
if (!req.session.adminAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM routes').then((rows) => {
|
|
res.json(rows);
|
|
}).catch(err => {
|
|
console.error('Error getting routes:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get('/api/v1/admin/route/:id', (req, res) => { // Get route
|
|
if (!req.session.adminAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM routes WHERE id = ?', [req.params.id]).then((rows) => {
|
|
const row = rows[0];
|
|
if (!row) {
|
|
res.status(404).json({ error: 'Not Found' });
|
|
return;
|
|
}
|
|
res.json(row);
|
|
}).catch(err => {
|
|
console.error('Error getting route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.post('/api/v1/admin/route', (req, res) => { // Create a new route
|
|
if (!req.session.adminAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
const server = req.body.server;
|
|
const port = req.body.port;
|
|
const auth = req.body.auth || "from-astrocom";
|
|
const secret = req.body.secret || crypto.randomBytes(15).toString('hex');
|
|
const block_start = req.body.block_start;
|
|
const block_length = req.body.block_length || 9999;
|
|
const apiKey = crypto.randomBytes(32).toString('hex');
|
|
const contact = req.body.contact || "Unknown";
|
|
// Validate all inputs exist
|
|
if (!server || !port || !block_start) {
|
|
res.status(400).json({ error: 'Bad Request' });
|
|
return;
|
|
}
|
|
// Check if route already exists (OR conditions on server, and block range)
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM routes WHERE block_start <= ? AND block_start + block_length >= ?', [block_start, block_start]).then((rows) => {
|
|
const row = rows[0];
|
|
if (row) {
|
|
res.status(409).json({ error: 'Conflict' });
|
|
return;
|
|
} else {
|
|
conn.query('INSERT INTO routes (server, port, auth, secret, block_start, block_length, apiKey, contact) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[server, port, auth, secret, block_start, block_length, apiKey, contact]).then(() => {
|
|
res.status(201).json({ message: 'Created' });
|
|
}).catch(err => {
|
|
console.error('Error creating route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
}
|
|
}).catch(err => {
|
|
console.error('Error checking for existing route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.put('/api/v1/admin/route/:id', (req, res) => { // Update a route
|
|
// Check if authenticated
|
|
if (!req.session.adminAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
// Check if route exists
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM routes WHERE id = ?', [req.params.id]).then((rows) => {
|
|
const row = rows[0];
|
|
if (!row) {
|
|
res.status(404).json({ error: 'Not Found' });
|
|
return;
|
|
}
|
|
// Update route
|
|
const server = req.body.server || row.server;
|
|
const port = req.body.port || row.port;
|
|
const auth = req.body.auth || row.auth;
|
|
const secret = req.body.secret || row.secret;
|
|
const block_start = req.body.block_start || row.block_start;
|
|
const block_length = req.body.block_length || row.block_length;
|
|
const contact = req.body.contact || row.contact;
|
|
console.log(`Updating ${req.params.id} to ${server}:${port} with ${auth}:${secret} for ${block_start} - ${block_start + block_length}. Contact: ${contact}`);
|
|
conn.query('UPDATE routes SET server = ?, port = ?, auth = ?, secret = ?, block_start = ?, block_length = ?, contact = ? WHERE id = ?',
|
|
[server, port, auth, secret, block_start, block_length, contact, req.params.id]).then(() => {
|
|
res.json({ message: 'Updated' });
|
|
}).catch(err => {
|
|
console.error('Error updating route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
}).catch(err => {
|
|
console.error('Error getting route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.delete('/api/v1/admin/route/:id', (req, res) => { // Delete a route
|
|
if (!req.session.adminAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
pool.getConnection().then(async conn => {
|
|
await conn.query('DELETE FROM directory WHERE route = ?', [req.params.id])
|
|
conn.query('DELETE FROM routes WHERE id = ?', [req.params.id]).then(() => {
|
|
res.json({ message: 'Deleted' });
|
|
}).catch(err => {
|
|
console.error('Error deleting route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.delete('/api/v1/admin/directory/:number', (req, res) => { // Delete a directory entry
|
|
if (!req.session.adminAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
const number = Number(req.params.number);
|
|
if (!number) {
|
|
res.status(400).json({ error: 'Bad Request' });
|
|
return;
|
|
}
|
|
pool.getConnection().then(conn => {
|
|
conn.query('DELETE FROM directory WHERE number = ?', [number]).then(() => {
|
|
res.status(200).json({ message: 'Deleted' });
|
|
}).catch(err => {
|
|
console.error('Error deleting directory entry:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get("/api/v1/admin/callLogs", (req, res) => {
|
|
if (!req.session.adminAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
// if ?page is set, return 100 results from that page, if no page assume page 1
|
|
const page = Number(req.query.page) || 1;
|
|
const offset = (page - 1) * 100;
|
|
// Get full count of call logs to calculate total pages
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT COUNT(*) as count FROM callLogs").then((rows) => {
|
|
const totalPages = Math.ceil(rows[0].count / 100);
|
|
conn.query("SELECT * FROM callLogs ORDER BY timestamp DESC LIMIT 100 OFFSET ?", [offset]).then((rows) => {
|
|
res.json({ totalPages, page, data: rows });
|
|
}).catch(err => {
|
|
console.error('Error getting call logs:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
}).catch(err => {
|
|
console.error('Error getting call log count:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
// == END ADMIN ROUTES ==
|
|
|
|
// == User routes == // allows someone to log in with their API key and add entries to the Directory (as long as the number is within their block range)
|
|
app.get('/user', (req, res) => {
|
|
if (!req.session.userAuthenticated) {
|
|
res.redirect('/user/login');
|
|
return;
|
|
}
|
|
res.render('user/index', { user: req.session.user });
|
|
});
|
|
|
|
app.get('/user/login', (req, res) => {
|
|
res.render('user/login');
|
|
});
|
|
|
|
app.post('/user/login', (req, res) => {
|
|
const apiKey = req.body.apiKey;
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT * FROM routes WHERE apiKey = ?", [apiKey]).then((rows) => {
|
|
const row = rows[0];
|
|
if (!row) {
|
|
res.status(401).send('Unauthorized');
|
|
return;
|
|
}
|
|
req.session.userAuthenticated = true;
|
|
req.session.userData = row;
|
|
res.redirect('/user');
|
|
}).catch(err => {
|
|
console.error('Error getting route:', err);
|
|
res.status(500).send('Internal server error');
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get('/user/logout', (req, res) => {
|
|
req.session.destroy();
|
|
res.redirect('/user/login');
|
|
});
|
|
|
|
app.get("/user/edit", (req, res) => {
|
|
if (!req.session.userAuthenticated) {
|
|
res.redirect('/user/login');
|
|
return;
|
|
}
|
|
// Remove block_start, block_length, and apiKey from the response
|
|
responseData = {
|
|
server: req.session.userData.server,
|
|
port: req.session.userData.port,
|
|
auth: req.session.userData.auth,
|
|
secret: req.session.userData.secret
|
|
}
|
|
res.render('user/edit', { data: responseData });
|
|
});
|
|
|
|
app.get('/api/v1/user/route', (req, res) => { // Get route
|
|
if (!req.session.userAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
res.json(req.session.userData);
|
|
});
|
|
|
|
app.put('/api/v1/user/route', (req, res) => { // Update route
|
|
if (!req.session.userAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
if (!req.session.userData.apiKey) {
|
|
req.session.destroy(); // Something weird happened, destroy session
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
// Does not allow for ID to be specified, always update current users route
|
|
const server = req.body.server || req.session.userData.server;
|
|
const port = req.body.port || req.session.userData.port;
|
|
const auth = req.body.auth || req.session.userData.auth;
|
|
const secret = req.body.secret || req.session.userData.secret;
|
|
pool.getConnection().then(conn => {
|
|
conn.query('UPDATE routes SET server = ?, port = ?, auth = ?, secret = ? WHERE apiKey = ?',
|
|
[server, port, auth, secret, req.session.userData.apiKey]).then(() => {
|
|
req.session.userData.server = server;
|
|
req.session.userData.port = port;
|
|
req.session.userData.auth = auth;
|
|
req.session.userData.secret = secret;
|
|
res.json({ message: 'Updated' });
|
|
}).catch(err => {
|
|
console.error('Error updating route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get('/api/v1/user/directory', (req, res) => { // Get directory entries created by user
|
|
if (!req.session.userAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM directory WHERE route = ?', [req.session.userData.id]).then((rows) => {
|
|
res.json(rows);
|
|
}).catch(err => {
|
|
console.error('Error getting routes:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.post('/api/v1/user/directory', (req, res) => { // Create a new directory entry
|
|
// Check if authenticated
|
|
if (!req.session.userAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
// Check that the number is within the block range for the current user
|
|
var number = Number(req.body.number);
|
|
var name = String(req.body.name);
|
|
if (!number || !name) {
|
|
res.status(400).json({ error: 'Bad Request' });
|
|
return;
|
|
}
|
|
|
|
if (number < req.session.userData.block_start || number > req.session.userData.block_start + req.session.userData.block_length) {
|
|
res.status(403).json({ error: 'Forbidden' });
|
|
return;
|
|
}
|
|
|
|
// Remove html
|
|
name = require("escape-html")(name);
|
|
|
|
const route = req.session.userData.id;
|
|
// If number already exists, update, otherwise insert
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM directory WHERE number = ? AND route = ?', [number, route]).then((rows) => {
|
|
const row = rows[0];
|
|
if (row) {
|
|
conn.query('UPDATE directory SET name = ? WHERE number = ? AND route = ?',
|
|
[name, number, route]).then(() => {
|
|
res.json({ message: 'Updated' });
|
|
}).catch(err => {
|
|
console.error('Error updating directory entry:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
} else {
|
|
conn.query('INSERT INTO directory (number, name, route) VALUES (?, ?, ?)',
|
|
[number, name, route]).then(() => {
|
|
res.status(201).json({ message: 'Created' });
|
|
}).catch(err => {
|
|
console.error('Error creating directory entry:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
}
|
|
}).catch(err => {
|
|
console.error('Error checking for existing directory entry:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.delete('/api/v1/user/directory/:number', (req, res) => { // Delete a directory entry
|
|
if (!req.session.userAuthenticated) {
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
const number = Number(req.params.number);
|
|
if (!number) {
|
|
res.status(400).json({ error: 'Bad Request' });
|
|
return;
|
|
}
|
|
pool.getConnection().then(conn => {
|
|
conn.query('DELETE FROM directory WHERE number = ? AND route = ?', [number, req.session.userData.id]).then(() => {
|
|
res.status(200).json({ message: 'Deleted' });
|
|
}).catch(err => {
|
|
console.error('Error deleting directory entry:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
// == END USER ROUTES ==
|
|
|
|
// == Directory routes == (unauthenticated)
|
|
|
|
app.get("/api/v1/directory", (req, res) => {
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT * FROM directory").then((rows) => {
|
|
res.json(rows);
|
|
}).catch(err => {
|
|
console.error('Error getting directory:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Function to find open number blocks
|
|
app.get("/api/v1/directory/openBlocks", (req, res) => {
|
|
pool.query("SELECT block_start, block_length FROM routes").then((rows) => {
|
|
console.log(JSON.stringify(rows)); // for testing
|
|
|
|
const takenBlocks = rows.map(row => {
|
|
return { start: row.block_start, end: row.block_start + row.block_length };
|
|
});
|
|
const openBlocks = [];
|
|
for (let i = 1000000; i <= 9999999; i += 10000) {
|
|
const blockStart = i;
|
|
const blockEnd = i + 9999;
|
|
// Check if block is invalid
|
|
if (invalidBlocks.includes(blockStart)) {
|
|
continue;
|
|
}
|
|
// Check if block overlaps with any taken blocks
|
|
const overlap = takenBlocks.some(taken => {
|
|
return (blockStart <= taken.end && blockEnd >= taken.start);
|
|
});
|
|
if (!overlap) {
|
|
openBlocks.push(blockStart);
|
|
}
|
|
}
|
|
res.json(openBlocks);
|
|
}).catch(err => {
|
|
console.error('Error getting open blocks:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
});
|
|
|
|
// Other public endpoints that need special handling
|
|
discordInviteCache = { time: 0, url: "" };
|
|
|
|
app.get("/discord", (req, res) => {
|
|
// fetch from process.env.WIDGET_URL, get json body, redirect to body.instant_invite. Cache url for 5 minutes
|
|
if (Date.now() - discordInviteCache.time < 300000) {
|
|
res.redirect(discordInviteCache.url);
|
|
return;
|
|
}
|
|
fetch(process.env.WIDGET_URL)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
discordInviteCache.time = Date.now();
|
|
discordInviteCache.url = data.instant_invite;
|
|
res.redirect(data.instant_invite);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching discord invite:', error);
|
|
res.status(500).send('Internal server error');
|
|
});
|
|
});
|
|
|
|
app.get("/api/analytics", (req, res) => {
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT * FROM analytics").then((total) => {
|
|
conn.query("SELECT * FROM dailyAnalytics").then((daily) => {
|
|
// Find the latest date and add "current:true" to it
|
|
var latest = { tag_date: "1970-01-01", count: 0 };
|
|
daily.forEach((entry) => {
|
|
if (entry.tag_date > latest.tag_date) {
|
|
latest = entry;
|
|
}
|
|
});
|
|
latest.current = true;
|
|
res.json({ total, daily });
|
|
}).catch(err => {
|
|
console.error('Error getting daily analytics:', err);
|
|
res.status(500).send('Internal server error');
|
|
});
|
|
}).catch(err => {
|
|
console.error('Error getting analytics:', err);
|
|
res.status(500).send('Internal server error');
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get("/footer", (req, res) => {
|
|
res.render("footer", { version });
|
|
});
|
|
|
|
app.get("/api/v1/checkAvailability/:number", (req, res) => {
|
|
// Check if the number is 7 digits
|
|
let number = Number(req.params.number);
|
|
// Round to nearest 10000 so it's always NXX0000
|
|
number = Math.floor(number / 10000) * 10000;
|
|
if (!number || number < 1000000 || number > 9999999 || invalidBlocks.includes(number)) {
|
|
res.status(400).json({ error: `Number is outside valid range or is an invalid block` });
|
|
return;
|
|
}
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM routes WHERE block_start <= ? AND block_start + block_length >= ?', [number, number]).then((rows) => {
|
|
const row = rows[0];
|
|
if (row) {
|
|
res.json({ available: false, block: row.block_start });
|
|
} else {
|
|
res.json({ available: true });
|
|
}
|
|
}).catch(err => {
|
|
console.error('Error getting route:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get("/api/healthcheck", (req, res) => {
|
|
// Check ability to connect to database with select * from routes
|
|
pool.getConnection().then(conn => {
|
|
conn.query('SELECT * FROM routes').then(() => {
|
|
res.status(200).send('OK');
|
|
}).catch(err => {
|
|
console.error('Error checking health:', err);
|
|
res.status(500).send('Internal server error');
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
// logCall function (caller, callee)
|
|
const logCall = (caller, callee) => {
|
|
pool.getConnection().then(conn => {
|
|
conn.query('INSERT INTO callLogs (caller, callee, timestamp) VALUES (?, ?, ?)',
|
|
[caller, callee, Math.floor(Date.now())]).catch(err => {
|
|
console.error('Error logging call:', err);
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Query to get a route
|
|
app.get('/api/v1/route/:apiKey/:ani/:number', (req, res) => {
|
|
const apiKey = req.params.apiKey;
|
|
const number = Number(req.params.number);
|
|
const ani = Number(req.params.ani);
|
|
pool.getConnection().then(conn => {
|
|
//conn.query("SELECT * FROM routes WHERE apiKey = ? AND block_start <= ? AND block_start + block_length >= ?", [apiKey, ani, ani]).then((rows) => {
|
|
conn.query("SELECT * FROM routes WHERE apiKey = ?", [apiKey]).then((rows) => { // We'll try this Nick, if it doesn't work we'll go back to the original
|
|
const row = rows[0];
|
|
// If no row or error, return 401
|
|
if (!row) {
|
|
res.status(401).send(`${process.env.MSG_ROUTE_ADDRESS}/401`)
|
|
return;
|
|
}
|
|
conn.query('SELECT * FROM routes WHERE block_start <= ? AND block_start + block_length >= ?', [number, number]).then((rows) => {
|
|
const row = rows[0];
|
|
if (row) {
|
|
// Check if the ANI is within the block range
|
|
// If it is, return `local`
|
|
console.log(`New Call: ${ani} -> ${number}`);
|
|
logCall(ani, number);
|
|
// incriment estCallsMade analytics
|
|
addAnalytic("estCallsMade");
|
|
dailyAnalytic("dailyCallsMade");
|
|
if (ani >= row.block_start && ani <= row.block_start + row.block_length) {
|
|
res.status(200).send('local');
|
|
} else {
|
|
res.status(200).send(`IAX2/${row.auth}:${row.secret}@${row.server}:${row.port}/${number}`);
|
|
}
|
|
} else {
|
|
res.status(404).send(`${process.env.MSG_ROUTE_ADDRESS}/404`);
|
|
}
|
|
}).catch(err => {
|
|
console.error('Error getting route:', err);
|
|
res.status(500).send(`${process.env.MSG_ROUTE_ADDRESS}/500`)
|
|
});
|
|
}).catch(err => {
|
|
console.error(err);
|
|
res.status(401).send(`${process.env.MSG_ROUTE_ADDRESS}/401`)
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get('/api/v1', (req, res) => { // Backwards compatibility with TandmX cause why not, it's easy
|
|
const apiKey = req.query.auth;
|
|
const number = Number(req.query.number);
|
|
const ani = Number(req.query.ani);
|
|
pool.getConnection().then(conn => {
|
|
conn.query("SELECT * FROM routes WHERE apiKey = ? AND block_start <= ? AND block_start + block_length >= ?", [apiKey, ani, ani]).then((rows) => {
|
|
const row = rows[0];
|
|
// If no row or error, return 401
|
|
if (!row) {
|
|
res.status(401).send(`${process.env.MSG_ROUTE_ADDRESS}/401`)
|
|
return;
|
|
}
|
|
conn.query('SELECT * FROM routes WHERE block_start <= ? AND block_start + block_length >= ?', [number, number]).then((rows) => {
|
|
const row = rows[0];
|
|
if (row) {
|
|
// Check if the ANI is within the block range
|
|
// If it is, return `local`
|
|
console.log(`New Call: ${ani} -> ${number}`);
|
|
logCall(ani, number);
|
|
addAnalytic("estCallsMade");
|
|
dailyAnalytic("dailyCallsMade");
|
|
if (ani >= row.block_start && ani <= row.block_start + row.block_length) {
|
|
res.status(200).send('local');
|
|
} else {
|
|
res.status(200).send(`IAX2/${row.auth}:${row.secret}@${row.server}:${row.port}/${number}`);
|
|
}
|
|
} else {
|
|
res.status(404).send(`${process.env.MSG_ROUTE_ADDRESS}/404`);
|
|
}
|
|
}).catch(err => {
|
|
console.error('Error getting route:', err);
|
|
res.status(500).send(`${process.env.MSG_ROUTE_ADDRESS}/500`)
|
|
});
|
|
}).catch(err => {
|
|
console.error(err);
|
|
res.status(401).send(`${process.env.MSG_ROUTE_ADDRESS}/401`)
|
|
}).finally(() => {
|
|
conn.release();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Management Routes (Like restarting the server)
|
|
app.post("/api/v1/manage/restart", (req, res) => {
|
|
// Check Authorization header against process.env.MANAGEMENT_AUTH
|
|
if (req.headers.authorization !== (process.env.MANAGEMENT_AUTH || crypto.randomBytes(32).toString('hex'))) { // Default to random in case we forget to configure it
|
|
res.status(401).send("Unauthorized");
|
|
return;
|
|
}
|
|
res.status(200).send("Restarting server...");
|
|
setTimeout(() => {
|
|
process.exit(0);
|
|
}, 500);
|
|
});
|
|
|
|
// Start server
|
|
app.listen(port, () => {
|
|
console.log(`Listening on port ${port}`);
|
|
}); |