Compare commits

...

3 commits
dev ... main

4 changed files with 235 additions and 26 deletions

View file

@ -831,7 +831,7 @@ app.post('/api/v1/user/dir/massUpdate', async (req, res) => {
return; return;
} }
} }
if(replace) { if (replace) {
// Delete all existing entries for this route // Delete all existing entries for this route
await pool.query('DELETE FROM directory WHERE route = ?', [route.id]); await pool.query('DELETE FROM directory WHERE route = ?', [route.id]);
} }
@ -1071,6 +1071,23 @@ const genCall = (req, res, apiKey, ani, number) => {
conn.query('SELECT * FROM routes WHERE block_start <= ? AND block_start + block_length >= ?', [number, number]).then((rows) => { conn.query('SELECT * FROM routes WHERE block_start <= ? AND block_start + block_length >= ?', [number, number]).then((rows) => {
const row = rows[0]; const row = rows[0];
// Check blocklist. Type 1 is exact match, Type 2 is prefix match NNNXXXX where NNN is the prefix value.
// Check if the ANI is blocked from calling this route
const routeId = row ? row.id : null;
if (!routeId) {
res.status(404).send(`${process.env.MSG_ROUTE_ADDRESS}/404`);
return;
}
conn.query('SELECT * FROM blocklist WHERE (blockType = 1 AND blockValue = ?) OR (blockType = 2 AND ? BETWEEN blockValue AND blockValue + ?);', [ani, ani, row.block_length]).then((blockRows) => {
if (blockRows.length > 0) {
// ANI is blocked from calling this route
console.log(`Blocked Call Attempt: ${ani} -> ${number}`);
res.status(403).send(`${process.env.MSG_ROUTE_ADDRESS}/403`);
return;
}
if (row) { if (row) {
// Check if the ANI is within the block range // Check if the ANI is within the block range
// If it is, return `local` // If it is, return `local`
@ -1087,6 +1104,11 @@ const genCall = (req, res, apiKey, ani, number) => {
} else { } else {
res.status(404).send(`${process.env.MSG_ROUTE_ADDRESS}/404`); res.status(404).send(`${process.env.MSG_ROUTE_ADDRESS}/404`);
} }
}).catch(err => {
console.error('Error checking blocklist:', err);
res.status(500).send(`${process.env.MSG_ROUTE_ADDRESS}/500`);
return;
});
}).catch(err => { }).catch(err => {
console.error('Error getting route:', err); console.error('Error getting route:', err);
res.status(500).send(`${process.env.MSG_ROUTE_ADDRESS}/500`) res.status(500).send(`${process.env.MSG_ROUTE_ADDRESS}/500`)

View file

@ -59,7 +59,7 @@ function runMigrations(pool) {
resolve(); resolve();
}) })
.catch(err => { .catch(err => {
console.errorr('Error running migrations:', err); console.error('Error running migrations:', err);
reject(err); reject(err);
}) })
.finally(() => { .finally(() => {

View file

@ -0,0 +1,8 @@
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
);

179
public/docs/index.html Normal file
View file

@ -0,0 +1,179 @@
<!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 &lt;API_KEY&gt;)</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 &lt;API_KEY&gt;.</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,0009,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>