Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
013d6d4c62 | ||
|
|
2f849f63e7 |
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -131,6 +131,4 @@ dist
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
sessions/*
|
sessions/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
test/*
|
|
||||||
60
analytics.js
Normal file
60
analytics.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
const pool = global.db_pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAnalytic = (tag) => {
|
||||||
|
pool.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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const logCall = (caller, callee) => {
|
||||||
|
pool.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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addAnalytic,
|
||||||
|
dailyAnalytic,
|
||||||
|
logCall
|
||||||
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ function runMigrations(pool) {
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Error running migrations:', err);
|
console.errorr('Error running migrations:', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS admin_invites (
|
|
||||||
code VARCHAR(36) PRIMARY KEY NOT NULL DEFAULT (UUID()),
|
|
||||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
maxUses INTEGER NOT NULL DEFAULT 1,
|
|
||||||
uses INTEGER NOT NULL DEFAULT 0,
|
|
||||||
expiresAt TIMESTAMP,
|
|
||||||
createdBy INTEGER,
|
|
||||||
FOREIGN KEY (createdBy) REFERENCES users(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS blocklist (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
ownerId INT NOT NULL,
|
|
||||||
blockType INT NOT NULL,
|
|
||||||
blockValue VARCHAR(255) NOT NULL,
|
|
||||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (ownerId) REFERENCES routes(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
1402
package-lock.json
generated
1402
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,18 +5,22 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.chrischro.me/AstroCom/AstroCom-API"
|
||||||
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.4.7",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"mariadb": "^3.4.0",
|
"mariadb": "^3.4.0",
|
||||||
"session-file-store": "^1.5.0"
|
"sqlite3": "^5.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,6 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
|
||||||
<title>AstroCom Directory</title>
|
<title>AstroCom Directory</title>
|
||||||
<script>
|
|
||||||
(function (d, t) {
|
|
||||||
var BASE_URL = "https://support.chrischro.me";
|
|
||||||
var g = d.createElement(t), s = d.getElementsByTagName(t)[0];
|
|
||||||
g.src = BASE_URL + "/packs/js/sdk.js";
|
|
||||||
g.defer = true;
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g, s);
|
|
||||||
g.onload = function () {
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: '1Epwwnhnmieqzu2dm3jYH3Qp',
|
|
||||||
baseUrl: BASE_URL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})(document, "script");
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-dark text-white">
|
<body class="bg-dark text-white">
|
||||||
|
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
||||||
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
|
|
||||||
<title>AstroCom API Docs</title>
|
|
||||||
<script>
|
|
||||||
(function (d, t) {
|
|
||||||
var BASE_URL = "https://support.chrischro.me";
|
|
||||||
var g = d.createElement(t), s = d.getElementsByTagName(t)[0];
|
|
||||||
g.src = BASE_URL + "/packs/js/sdk.js";
|
|
||||||
g.defer = true;
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g, s);
|
|
||||||
g.onload = function () {
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: '1Epwwnhnmieqzu2dm3jYH3Qp',
|
|
||||||
baseUrl: BASE_URL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})(document, "script");
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.doc-section { max-width: 900px; margin: 1.5rem auto; }
|
|
||||||
.endpoint { background: rgba(255,255,255,0.03); padding: 1rem; border-radius: .4rem; margin-bottom: .75rem; }
|
|
||||||
.code { background:#0d1117; color:#9ad8ff; padding:.5rem; border-radius:.25rem; font-family:monospace; white-space:pre-wrap; }
|
|
||||||
.small-muted { color: rgba(255,255,255,0.6); font-size:.9rem; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="bg-dark text-white">
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="/">AstroCom</a>
|
|
||||||
<span id="footer"></span>
|
|
||||||
</div>
|
|
||||||
<div class="ms-auto d-flex text-nowrap">
|
|
||||||
<a href="/user" class="btn btn-outline-light me-2">User Login</a>
|
|
||||||
<a href="/admin" class="btn btn-outline-light">Admin Login</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container doc-section">
|
|
||||||
<h2 class="mb-1">API Documentation</h2>
|
|
||||||
<p class="small-muted">This page lists only endpoints that are fully unauthenticated or accept an API key via Bearer token.</p>
|
|
||||||
|
|
||||||
<h4 class="mt-4">Unauthenticated (public) endpoints</h4>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>GET /api/v1/directory</h5>
|
|
||||||
<p class="small-muted">Returns all directory entries.</p>
|
|
||||||
<div class="mb-2"><strong>Request</strong></div>
|
|
||||||
<div class="code">curl -s -X GET https://astrocom.tel/api/v1/directory</div>
|
|
||||||
<div class="mb-2 mt-2"><strong>Response (200)</strong></div>
|
|
||||||
<div class="code">[{"id":1,"number":4472000,"name":"Example","route":2}, ...]</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>GET /api/v1/directory/openBlocks</h5>
|
|
||||||
<p class="small-muted">Returns a list of available 10k blocks (block start numbers).</p>
|
|
||||||
<div class="code">curl -s https://astrocom.tel/api/v1/directory/openBlocks</div>
|
|
||||||
<div class="code">[1000000,1010000, ...]</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>GET /api/v1/checkAvailability/:number</h5>
|
|
||||||
<p class="small-muted">Checks availability for a 7-digit number (rounded to NXX0000). Returns available: true/false.</p>
|
|
||||||
<div class="code">curl -s https://astrocom.tel/api/v1/checkAvailability/4472001</div>
|
|
||||||
<div class="code">{"available":true}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>GET /api/analytics</h5>
|
|
||||||
<p class="small-muted">Public analytics (total and daily counts).</p>
|
|
||||||
<div class="code">curl -s https://astrocom.tel/api/analytics</div>
|
|
||||||
<div class="code">{"total":[{"tag":"apiCalls","count":123}], "daily":[{"tag":"apiCalls","tag_date":"2025-10-27","count":10}]}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>GET /discord</h5>
|
|
||||||
<p class="small-muted">Redirects to the configured Discord invite (server-side fetch from WIDGET_URL).</p>
|
|
||||||
<div class="code">curl -i https://astrocom.tel/discord</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>GET /api/v1/provision/:apiKey</h5>
|
|
||||||
<p class="small-muted">Provisioning info for a route identified by API key. Returns server/port/iax creds and block.</p>
|
|
||||||
<div class="code">curl -s https://astrocom.tel/api/v1/provision/REPLACE_API_KEY</div>
|
|
||||||
<div class="code">{
|
|
||||||
"server":"iax.example.net",
|
|
||||||
"port":4569,
|
|
||||||
"inbound_context":"from-astrocom",
|
|
||||||
"iax_secret":"...secret...",
|
|
||||||
"block":4470000,
|
|
||||||
"api_key":"REPLACE_API_KEY"
|
|
||||||
}</div>
|
|
||||||
<p class="small-muted">Response may include "warning" if DNS IP doesn't match requester.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>GET /api/v1/route/:apiKey/:ani/:number</h5>
|
|
||||||
<p class="small-muted">Primary routing endpoint. Returns "local" or an IAX2 dial string for the callee.</p>
|
|
||||||
<div class="code">curl -s https://astrocom.tel/api/v1/route/REPLACE_API_KEY/4472001/4473005</div>
|
|
||||||
<div class="code">local
|
|
||||||
-- or --
|
|
||||||
IAX2/from-astrocom:secret@iax.example.net:4569/4473005</div>
|
|
||||||
<p class="small-muted">Also available as legacy query form:</p>
|
|
||||||
<div class="code">GET /api/v1?auth=APIKEY&ani=4472001&number=4473005</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="border-secondary">
|
|
||||||
|
|
||||||
<h4 class="mt-4">Bearer token endpoints (Authorization: Bearer <API_KEY>)</h4>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>PATCH /api/v1/user/update</h5>
|
|
||||||
<p class="small-muted">Update server/port/auth/secret for the route identified by Bearer API key (used by automated scripts).</p>
|
|
||||||
<div class="code">curl -s -X PATCH \
|
|
||||||
-H "Authorization: Bearer REPLACE_API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"server":"iax.example.net","port":4569,"auth":"from-astrocom","secret":"new-secret"}' \
|
|
||||||
https://astrocom.tel/api/v1/user/update</div>
|
|
||||||
<div class="code">{"message":"Updated"}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>POST /api/v1/user/dir/newEntry</h5>
|
|
||||||
<p class="small-muted">Create or update a single directory entry for the route belonging to the API key.</p>
|
|
||||||
<div class="code">curl -s -X POST \
|
|
||||||
-H "Authorization: Bearer REPLACE_API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"number":4472005,"name":"Alice"}' \
|
|
||||||
https://astrocom.tel/api/v1/user/dir/newEntry</div>
|
|
||||||
<div class="code">{"message":"Created"} or {"message":"Updated"}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>DELETE /api/v1/user/dir/deleteEntry/:number</h5>
|
|
||||||
<p class="small-muted">Delete a directory entry owned by the API key's route.</p>
|
|
||||||
<div class="code">curl -s -X DELETE -H "Authorization: Bearer REPLACE_API_KEY" https://astrocom.tel/api/v1/user/dir/deleteEntry/4472005</div>
|
|
||||||
<div class="code">{"message":"Deleted"}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h5>POST /api/v1/user/dir/massUpdate</h5>
|
|
||||||
<p class="small-muted">Mass-insert/update directory entries. Body must be {"entries":[{number,name},...],"replace":true|false}.</p>
|
|
||||||
<div class="code">curl -s -X POST \
|
|
||||||
-H "Authorization: Bearer REPLACE_API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"replace":false,"entries":[{"number":4472001,"name":"Bob"},{"number":4472002,"name":"Carol"}]}' \
|
|
||||||
https://astrocom.tel/api/v1/user/dir/massUpdate</div>
|
|
||||||
<div class="code">{"message":"Mass update completed"}</div>
|
|
||||||
<p class="small-muted">All numbers must be within the route's block range.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="border-secondary">
|
|
||||||
|
|
||||||
<p class="small-muted">Notes:</p>
|
|
||||||
<ul class="small-muted">
|
|
||||||
<li>Bearer endpoints accept header Authorization: Bearer <API_KEY>.</li>
|
|
||||||
<li>Unauthenticated endpoints that accept an API key in the path/query do not require Authorization header.</li>
|
|
||||||
<li>All numeric "number" and "ani" values must be 7-digit integers (1,000,000–9,999,999) where applicable.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/assets/js/directory.js"></script>
|
|
||||||
<script src="/assets/js/bootstrap.min.js"></script>
|
|
||||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="/assets/js/jquery.min.js"></script>
|
|
||||||
<script>
|
|
||||||
$(function() {
|
|
||||||
$("#footer").load("/footer");
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -52,8 +52,6 @@
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="/about">About (WIP)</a><span> </span>
|
<a href="/about">About (WIP)</a><span> </span>
|
||||||
<a href="/directory">Directory</a><span> </span>
|
<a href="/directory">Directory</a><span> </span>
|
||||||
<a href="/validator">Block Availability</a> <span> </span>
|
|
||||||
<a href="/status" class="disabled" aria-disabled="true" tabindex="-1" style="pointer-events: none; opacity: 0.5;">Status (WIP)</a><span> </span>
|
|
||||||
<a href="/discord">Discord Server</a>
|
<a href="/discord">Discord Server</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,6 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
|
||||||
<title>AstroCom Availability Checker</title>
|
<title>AstroCom Availability Checker</title>
|
||||||
<script>
|
|
||||||
(function (d, t) {
|
|
||||||
var BASE_URL = "https://support.chrischro.me";
|
|
||||||
var g = d.createElement(t), s = d.getElementsByTagName(t)[0];
|
|
||||||
g.src = BASE_URL + "/packs/js/sdk.js";
|
|
||||||
g.defer = true;
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g, s);
|
|
||||||
g.onload = function () {
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: '1Epwwnhnmieqzu2dm3jYH3Qp',
|
|
||||||
baseUrl: BASE_URL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})(document, "script");
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-dark text-white">
|
<body class="bg-dark text-white">
|
||||||
|
|
@ -50,47 +34,6 @@
|
||||||
<button type="submit" class="btn btn-primary mt-3">Submit</button>
|
<button type="submit" class="btn btn-primary mt-3">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mt-4" style="max-width: 400px;">
|
|
||||||
<h4>Available Blocks</h4>
|
|
||||||
<table class="table table-dark table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th id="availHeader" scope="col">Available Blocks</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="availableBlocksTable">
|
|
||||||
<tr><td>Loading...</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
async function loadAvailableBlocks() {
|
|
||||||
const tableBody = document.getElementById('availableBlocksTable');
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/v1/directory/openBlocks');
|
|
||||||
const blocks = await res.json();
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
||||||
blocks.forEach(block => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
const cell = document.createElement('td');
|
|
||||||
cell.textContent = block;
|
|
||||||
row.appendChild(cell);
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
// Set header text to "Available Blocks (X total)" where X is the number of available blocks
|
|
||||||
document.getElementById('availHeader').textContent = `${blocks.length} Available Blocks`;
|
|
||||||
} else {
|
|
||||||
tableBody.innerHTML = '<tr><td>No blocks available</td></tr>';
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
tableBody.innerHTML = '<tr><td>Error loading blocks</td></tr>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadAvailableBlocks();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.querySelector('form').addEventListener('submit', async (e) => {
|
document.querySelector('form').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
89
routes/admin.js
Normal file
89
routes/admin.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
const pool = global.db_pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const app = new express.Router();
|
||||||
|
|
||||||
|
app.get('/logout', (req, res) => {
|
||||||
|
req.session.destroy();
|
||||||
|
res.redirect('/admin/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/login', (req, res) => {
|
||||||
|
res.render('admin/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
if (!req.session.adminAuthenticated) {
|
||||||
|
res.redirect('/admin/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.render('admin/index', { user: req.session.user });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/create', (req, res) => {
|
||||||
|
if (!req.session.adminAuthenticated) {
|
||||||
|
res.redirect('/admin/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.render('admin/create', { user: req.session.user });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/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('/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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
516
routes/api.js
Normal file
516
routes/api.js
Normal file
|
|
@ -0,0 +1,516 @@
|
||||||
|
const pool = global.db_pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const app = new express.Router();
|
||||||
|
|
||||||
|
const analytics = require('../analytics');
|
||||||
|
|
||||||
|
// Admin routes (Authenticated)
|
||||||
|
|
||||||
|
app.get('/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('/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('/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('/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('/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('/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("/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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// User routes (Authenticated)
|
||||||
|
|
||||||
|
app.get('/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('/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('/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('/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('/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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
|
||||||
|
app.get("/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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/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("/v1/checkAvailability/:number", (req, res) => {
|
||||||
|
// Check if the number is 7 digits
|
||||||
|
const number = Number(req.params.number);
|
||||||
|
if (number < 2000000 || number > 9999999) {
|
||||||
|
res.status(400).json({ error: `Number is outside valid range` });
|
||||||
|
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("/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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// AstroCom Asterisk routes (Authenticated)
|
||||||
|
|
||||||
|
app.get('/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}`);
|
||||||
|
analytics.logCall(ani, number);
|
||||||
|
// incriment estCallsMade analytics
|
||||||
|
analytics.addAnalytic("estCallsMade");
|
||||||
|
analytics.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('/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}`);
|
||||||
|
analytics.logCall(ani, number);
|
||||||
|
analytics.addAnalytic("estCallsMade");
|
||||||
|
analytics.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);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
58
routes/user.js
Normal file
58
routes/user.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
const pool = global
|
||||||
|
const express = require('express');
|
||||||
|
const app = new express.Router();
|
||||||
|
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# AstroCom Dynamic IP Update Script
|
|
||||||
# Gets current public IP from https://myip.wtf/text and posts it to the AstroCom API
|
|
||||||
# Requires: curl
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
API_KEY="Your ASTROCOM API Key" # Replace with your AstroCom API Key!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Get current IP
|
|
||||||
CURRENT_IP=$(curl -s https://myip.wtf/text)
|
|
||||||
if [[ -z "$CURRENT_IP" ]]; then
|
|
||||||
echo "Failed to retrieve current IP address."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Current IP: $CURRENT_IP"
|
|
||||||
# Update IP via AstroCom API PATCH https://astrocom.tel/api/v1/user/update; JSON body: {"server": "current_ip"}
|
|
||||||
curl -s -X PATCH https://astrocom.tel/api/v1/user/update -H "Content-Type: application/json" -H "Authorization: Bearer $API_KEY" -d "{\"server\": \"$CURRENT_IP\"}"
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
|
|
||||||
<title>AstroCom Admin Registration</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="bg-dark">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row justify-content-center mt-5">
|
|
||||||
<div class="col-md-6 col-lg-4">
|
|
||||||
<div class="card bg-dark text-light shadow">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<h2 class="text-center mb-4">Admin Registration</h2>
|
|
||||||
<% if (typeof notice !== 'undefined') { %>
|
|
||||||
<div class="alert alert-info text-center mb-3"><%= notice %></div>
|
|
||||||
<% } %>
|
|
||||||
<% if (typeof info !== 'undefined') { %>
|
|
||||||
<div class="alert alert-primary text-center mb-3"><%= info %></div>
|
|
||||||
<% } %>
|
|
||||||
<% if (typeof warn !== 'undefined') { %>
|
|
||||||
<div class="alert alert-warning text-center mb-3"><%= warn %></div>
|
|
||||||
<% } %>
|
|
||||||
<% if (typeof error !== 'undefined') { %>
|
|
||||||
<div class="alert alert-danger text-center mb-3"><%= error %></div>
|
|
||||||
<% } %>
|
|
||||||
<form action="#" method="POST">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label">Username:</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Password:</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-100">Register</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="footer" class="text-light mt-5"></div>
|
|
||||||
</div>
|
|
||||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="/assets/js/jquery.min.js"></script>
|
|
||||||
<script>
|
|
||||||
$(function() {
|
|
||||||
$("#footer").load("/footer");
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -2,20 +2,4 @@
|
||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
<span>© <%= new Date().getFullYear() %> AstroCom <%= version %></span>
|
<span>© <%= new Date().getFullYear() %> AstroCom <%= version %></span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
|
||||||
(function (d, t) {
|
|
||||||
var BASE_URL = "https://support.chrischro.me";
|
|
||||||
var g = d.createElement(t), s = d.getElementsByTagName(t)[0];
|
|
||||||
g.src = BASE_URL + "/packs/js/sdk.js";
|
|
||||||
g.defer = true;
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g, s);
|
|
||||||
g.onload = function () {
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: '1Epwwnhnmieqzu2dm3jYH3Qp',
|
|
||||||
baseUrl: BASE_URL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})(document, "script");
|
|
||||||
</script>
|
|
||||||
Loading…
Reference in a new issue