Compare commits
4 commits
11592d8bd1
...
3f0ff73d59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f0ff73d59 | ||
|
|
36a45e9812 | ||
|
|
2859349444 | ||
|
|
af1eaa3a57 |
147
index.js
147
index.js
|
|
@ -708,6 +708,153 @@ app.delete('/api/v1/user/directory/:number', (req, res) => { // Delete a directo
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// User directory management via API key, for automated scripts
|
||||||
|
app.post('/api/v1/user/dir/newEntry', async (req, res) => {
|
||||||
|
const apiKey = req.headers['authorization'] ? req.headers['authorization'].replace('Bearer ', '') : null;
|
||||||
|
if (!apiKey) {
|
||||||
|
res.status(401).json({ error: 'API Key is required!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeData = await pool.query("SELECT * FROM routes WHERE apiKey = ?", [apiKey]);
|
||||||
|
if (!routeData || routeData.length === 0) {
|
||||||
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = routeData[0];
|
||||||
|
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 < route.block_start || number > route.block_start + route.block_length) {
|
||||||
|
res.status(403).json({ error: 'Forbidden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove html
|
||||||
|
name = require("escape-html")(name);
|
||||||
|
// If number already exists, update, otherwise insert
|
||||||
|
pool.query('SELECT * FROM directory WHERE number = ? AND route = ?', [number, route.id]).then((rows) => {
|
||||||
|
const row = rows[0];
|
||||||
|
if (row) {
|
||||||
|
pool.query('UPDATE directory SET name = ? WHERE number = ? AND route = ?',
|
||||||
|
[name, number, route.id]).then(() => {
|
||||||
|
res.json({ message: 'Updated' });
|
||||||
|
}
|
||||||
|
).catch(err => {
|
||||||
|
console.error('Error updating directory entry:', err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pool.query('INSERT INTO directory (number, name, route) VALUES (?, ?, ?)',
|
||||||
|
[number, name, route.id]).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' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/v1/user/dir/deleteEntry/:number', async (req, res) => {
|
||||||
|
const apiKey = req.headers['authorization'] ? req.headers['authorization'].replace('Bearer ', '') : null;
|
||||||
|
if (!apiKey) {
|
||||||
|
res.status(401).json({ error: 'API Key is required!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeData = await pool.query("SELECT * FROM routes WHERE apiKey = ?", [apiKey]);
|
||||||
|
if (!routeData || routeData.length === 0) {
|
||||||
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = routeData[0];
|
||||||
|
const number = Number(req.params.number);
|
||||||
|
if (!number) {
|
||||||
|
res.status(400).json({ error: 'Bad Request' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check that the number is within the block range for the current user
|
||||||
|
if (number < route.block_start || number > route.block_start + route.block_length) {
|
||||||
|
res.status(403).json({ error: 'Forbidden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pool.query('DELETE FROM directory WHERE number = ? AND route = ?', [number, route.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' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// User directory endpoint to mass update entries, with boolean 'replace' field to indicate if existing entries should be replaced with the new list.
|
||||||
|
app.post('/api/v1/user/dir/massUpdate', async (req, res) => {
|
||||||
|
const apiKey = req.headers['authorization'] ? req.headers['authorization'].replace('Bearer ', '') : null;
|
||||||
|
if (!apiKey) {
|
||||||
|
res.status(401).json({ error: 'API Key is required!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const routeData = await pool.query("SELECT * FROM routes WHERE apiKey = ?", [apiKey]);
|
||||||
|
if (!routeData || routeData.length === 0) {
|
||||||
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all entries, make sure we check that each number is within the block range for the current user
|
||||||
|
|
||||||
|
|
||||||
|
const route = routeData[0];
|
||||||
|
const entries = req.body.entries;
|
||||||
|
const replace = req.body.replace || false;
|
||||||
|
if (!Array.isArray(entries)) {
|
||||||
|
res.status(400).json({ error: 'Bad Request. Not array' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const number = Number(entry.number);
|
||||||
|
const name = String(entry.name);
|
||||||
|
if (!number || !name) {
|
||||||
|
res.status(400).json({ error: 'Bad Request. Number Or Name' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (number < route.block_start || number > route.block_start + route.block_length) {
|
||||||
|
res.status(403).json({ error: 'Forbidden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(replace) {
|
||||||
|
// Delete all existing entries for this route
|
||||||
|
await pool.query('DELETE FROM directory WHERE route = ?', [route.id]);
|
||||||
|
}
|
||||||
|
// Insert or update entries
|
||||||
|
for (const entry of entries) {
|
||||||
|
const number = Number(entry.number);
|
||||||
|
const name = String(entry.name);
|
||||||
|
// Remove html
|
||||||
|
const safeName = require("escape-html")(name);
|
||||||
|
// If number already exists, update, otherwise insert
|
||||||
|
const existingEntries = await pool.query('SELECT * FROM directory WHERE number = ? AND route = ?', [number, route.id]);
|
||||||
|
const row = existingEntries[0];
|
||||||
|
if (row) {
|
||||||
|
await pool.query('UPDATE directory SET name = ? WHERE number = ? AND route = ?',
|
||||||
|
[safeName, number, route.id]);
|
||||||
|
} else {
|
||||||
|
await pool.query('INSERT INTO directory (number, name, route) VALUES (?, ?, ?)',
|
||||||
|
[number, safeName, route.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.json({ message: 'Mass update completed' });
|
||||||
|
});
|
||||||
|
|
||||||
// == END USER ROUTES ==
|
// == END USER ROUTES ==
|
||||||
|
|
||||||
// == Directory routes == (unauthenticated)
|
// == Directory routes == (unauthenticated)
|
||||||
|
|
|
||||||
1464
package-lock.json
generated
1464
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -11,14 +11,12 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"connect-sqlite": "^0.0.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",
|
"session-file-store": "^1.5.0"
|
||||||
"sqlite3": "^5.1.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
179
public/docs/index.html
Normal file
179
public/docs/index.html
Normal 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 <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>
|
||||||
Loading…
Reference in a new issue