Compare commits

..

41 commits

Author SHA1 Message Date
Christopher Cookman da5a029161 Merge pull request 'Impliment blocklist logic and db. TODO: Add web UI' (#2) from dev-blocklist into main
Reviewed-on: #2
2025-11-04 14:09:24 -07:00
Christopher Cookman 2594a7a8f7 Impliment blocklist logic and db. TODO: Add web UI 2025-11-04 13:04:44 -07:00
Christopher Cookman 3f0ff73d59 Add docs 2025-10-27 18:23:09 -06:00
Christopher Cookman 36a45e9812 Finished directory API. 2025-10-27 18:05:43 -06:00
Christopher Cookman 2859349444 Fix packages 2025-10-27 08:40:10 -06:00
Christopher Cookman af1eaa3a57 Add directory API endpoints 2025-10-27 08:04:46 -06:00
Christopher Cookman 11592d8bd1 Add DEBUG_MODE env var for some smol things 2025-10-04 23:20:07 -06:00
Christopher Cookman ba9013d9a9 Final touches! 2025-10-03 22:16:51 -06:00
Christopher Cookman b08c610a8f Update script to function 2025-10-03 22:14:51 -06:00
Christopher Cookman 76bd32a19f Whoops, forgot to remove Bearer 2025-10-03 22:13:55 -06:00
Christopher Cookman aa6ceb46b5 Gwug 2025-10-03 22:12:07 -06:00
Christopher Cookman 616bf2f29a Fix that possibly? 2025-10-03 22:10:23 -06:00
Christopher Cookman b4bd93c57d Add API to script updating individual servers user-side 2025-10-03 22:04:13 -06:00
Christopher Cookman a4b846f379 Gug 2025-10-03 00:16:17 -06:00
Christopher Cookman 89ad48bdd9 Im dumb 2025-10-03 00:13:11 -06:00
Christopher Cookman ee3ad51468 My dumb ass didnt have proper auth for calls. grr 2025-10-03 00:12:29 -06:00
Christopher Cookman 10322fefcc AAAA 2025-10-03 00:08:45 -06:00
Christopher Cookman ce027a8a30 gwug 2025-10-03 00:06:58 -06:00
Christopher Cookman eeca381dbd Gwug 2025-10-01 19:07:38 -06:00
Christopher Cookman a52a10378c WE LOVE TESTING ON PROD 2025-10-01 19:04:07 -06:00
Christopher Cookman cc62815961 Add prov api for future shell script 2025-09-30 03:48:55 -06:00
Christopher Cookman f12603387d Add another emerg number to blocklist 2025-09-29 22:05:28 -06:00
Christopher Cookman 3dc1dcfbef Fix const 2025-09-29 22:02:13 -06:00
Christopher Cookman e29320151a Update availability checker 2025-09-29 22:01:33 -06:00
Christopher Cookman cb93f44dbe Add home page buttons 2025-09-29 21:58:15 -06:00
Christopher Cookman be0d8907b5 Add avail blocks to validator 2025-09-29 21:57:12 -06:00
Christopher Cookman fc01718a32 Temp 2025-09-29 21:50:37 -06:00
Christopher Cookman 7ba88939b9 Add openBlocks API 2025-09-29 21:49:15 -06:00
Christopher Cookman 7d1a75b0fb Add chatwoot stuff 2025-03-22 21:51:41 -06:00
Christopher Cookman 31e7aa300a Delete dir entries when removing route, duh 2025-02-11 17:53:43 -07:00
Christopher Cookman a37629682c Fix for Nick 2024-12-27 21:26:16 -07:00
Christopher Cookman 95c94b0e6d Change API Key user login to type password 2024-12-26 11:37:24 -07:00
Christopher Cookman fe70d0aba3 Remove unneeded log 2024-12-19 13:13:34 -07:00
Christopher Cookman b611550ff5 Merge pull request 'Migrate to MariaDB' (#1) from mariadb-test into main
Reviewed-on: #1
2024-12-19 13:04:40 -07:00
Christopher Cookman 6743295eb2 Mariadb stuff 2024-12-19 13:02:45 -07:00
Christopher Cookman eb7a8b3bc5 Make changes needed to use mariadb; TODO: Full suite of testing 2024-12-19 10:29:00 -07:00
Christopher Cookman a3e3dfc501 Reformat admin page 2024-12-17 01:49:04 -07:00
Christopher Cookman eaaa81d9f2 Fix copyright/version text on most pages 2024-12-17 01:32:19 -07:00
Christopher Cookman 52c9d5c14d Add robots.txt 2024-12-17 01:17:02 -07:00
Christopher Cookman 44e0828fe8 Add copyright and version to most pages (bar landing page) 2024-12-17 01:15:25 -07:00
Christopher Cookman d9099943f0 Finish user editable fields 2024-12-16 21:39:32 -07:00
32 changed files with 1598 additions and 1717 deletions

4
.gitignore vendored
View file

@ -131,4 +131,6 @@ dist
*.db *.db
sessions/* sessions/*
.DS_Store .DS_Store
test/*

1079
index.js

File diff suppressed because it is too large Load diff

View file

@ -1,65 +1,72 @@
const sqlite3 = require('sqlite3').verbose(); const mariadb = require('mariadb');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const util = require("util"); const util = require("util")
function runMigrations(db) {
return new Promise((resolve, reject) => {
const migrationDir = path.join(__dirname, 'migrations');
const runQuery = util.promisify(db.run.bind(db)); function runMigrations(pool) {
const getQuery = util.promisify(db.get.bind(db)); return new Promise((resolve, reject) => {
let connection;
// Ensure a migrations table exists to track applied migrations pool.getConnection()
runQuery(`CREATE TABLE IF NOT EXISTS migrations ( .then(conn => {
id INTEGER PRIMARY KEY AUTOINCREMENT, connection = conn;
name TEXT NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`)
.then(() => {
// Read all migration files
const files = fs.readdirSync(migrationDir).sort(); // Sort to apply in order
return files.reduce((promise, file) => { // Ensure a migrations table exists to track applied migrations
return promise.then(() => { return connection.query(`CREATE TABLE IF NOT EXISTS migrations (
const migrationName = path.basename(file); id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`);
})
.then(() => {
// Read all migration files
const migrationDir = path.join(__dirname, 'migrations');
const files = fs.readdirSync(migrationDir).sort(); // Sort to apply in order
// Check if the migration has already been applied return files.reduce((promise, file) => {
return getQuery( return promise.then(() => {
'SELECT 1 FROM migrations WHERE name = ? LIMIT 1', const migrationName = path.basename(file);
[migrationName]
).then((row) => {
if (row) {
// console.log(`Skipping already applied migration: ${migrationName}`);
return; // Skip this migration
}
// Read and execute the migration SQL // Check if the migration has already been applied
const migrationPath = path.join(migrationDir, file); return connection.query(
const sql = fs.readFileSync(migrationPath, 'utf8'); 'SELECT 1 FROM migrations WHERE name = ? LIMIT 1',
[migrationName]
).then(([rows]) => {
if (Object.keys(rows || {}).length > 0) {
//console.log(`Skipping already applied migration: ${migrationName}`);
return; // Skip this migration
}
return runQuery(sql).then(() => { // Read and execute the migration SQL
// Record the applied migration const migrationPath = path.join(migrationDir, file);
return runQuery( const sql = fs.readFileSync(migrationPath, 'utf8');
'INSERT INTO migrations (name) VALUES (?)', return connection.query(sql).then(() => {
[migrationName] // Record the applied migration
).then(() => { return connection.query(
console.log(`Applied migration: ${migrationName}`); 'INSERT INTO migrations (name) VALUES (?)',
}); [migrationName]
}); ).then(() => {
}); console.log(`Applied migration: ${migrationName}`);
}); });
}, Promise.resolve()); });
}) });
.then(() => { });
console.log('All migrations applied successfully!'); }, Promise.resolve());
resolve(); })
}) .then(() => {
.catch((err) => { console.log('All migrations applied successfully!');
console.error('Error running migrations:', err); resolve();
reject(err); })
}) .catch(err => {
}); console.error('Error running migrations:', err);
reject(err);
})
.finally(() => {
if (connection) connection.release();
});
});
} }
module.exports = runMigrations;
module.exports = runMigrations

View file

@ -1,10 +1,10 @@
CREATE TABLE IF NOT EXISTS routes ( CREATE TABLE IF NOT EXISTS routes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTO_INCREMENT,
server TEXT NOT NULL, server VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 4569, port INTEGER NOT NULL DEFAULT 4569,
auth TEST NOT NULL DEFAULT 'from-astrocom', auth VARCHAR(255) NOT NULL DEFAULT 'from-astrocom',
secret TEXT NOT NULL, secret VARCHAR(255) NOT NULL,
block_start INTEGER UNIQUE NOT NULL, block_start INTEGER UNIQUE NOT NULL,
block_length INTEGER NOT NULL DEFAULT 9999, block_length INTEGER NOT NULL DEFAULT 9999,
apiKey TEXT NOT NULL apiKey VARCHAR(255) NOT NULL
) );

View file

@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTO_INCREMENT,
username TEXT NOT NULL, username VARCHAR(255) NOT NULL,
passwordHash TEXT NOT NULL passwordHash VARCHAR(255) NOT NULL
) );

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS directory ( CREATE TABLE IF NOT EXISTS directory (
number INTEGER PRIMARY KEY NOT NULL, -- This is the directory phone number number INTEGER PRIMARY KEY NOT NULL, -- This is the directory phone number
name TEXT NOT NULL, -- This is the text of the entry, set by the user. name VARCHAR(255) NOT NULL, -- This is the VARCHAR(255) of the entry, set by the user.
route INTEGER NOT NULL, -- This is the ID of the route that owns this entry. Foreign key to routes.id route INTEGER NOT NULL, -- This is the ID of the route that owns this entry. Foreign key to routes.id
FOREIGN KEY(route) REFERENCES routes(id) FOREIGN KEY(route) REFERENCES routes(id)
) );

View file

@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS analytics ( CREATE TABLE IF NOT EXISTS analytics (
tag TEXT NOT NULL PRIMARY KEY, tag VARCHAR(255) NOT NULL PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0 count INTEGER NOT NULL DEFAULT 0
); );

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS dailyAnalytics ( CREATE TABLE IF NOT EXISTS dailyAnalytics (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTO_INCREMENT,
tag TEXT NOT NULL, tag VARCHAR(255) NOT NULL,
count INTEGER NOT NULL DEFAULT 0, count INTEGER NOT NULL DEFAULT 0,
tag_date TEXT NOT NULL tag_date VARCHAR(255) NOT NULL
); );

View file

@ -1,6 +1,6 @@
CREATE TABLE callLogs ( CREATE TABLE callLogs (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTO_INCREMENT,
timestamp TEXT NOT NULL, timestamp VARCHAR(255) NOT NULL,
caller TEXT NOT NULL, caller VARCHAR(255) NOT NULL,
callee TEXT NOT NULL callee VARCHAR(255) NOT NULL
); );

View file

@ -1,2 +1 @@
ALTER TABLE routes ALTER TABLE routes ADD COLUMN contact VARCHAR(255);
ADD COLUMN contact TEXT;

View file

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

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
);

1463
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,13 +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",
"session-file-store": "^1.5.0", "mariadb": "^3.4.0",
"sqlite3": "^5.1.7" "session-file-store": "^1.5.0"
} }
} }

View file

@ -9,6 +9,8 @@ function getDirectoryEntries() {
row.innerHTML = `<td>${entry.number}</td><td>${entry.name}</td><td><button class="btn btn-danger" onclick="deleteDirectoryEntry(${entry.number})">Delete</button></td>`; row.innerHTML = `<td>${entry.number}</td><td>${entry.name}</td><td><button class="btn btn-danger" onclick="deleteDirectoryEntry(${entry.number})">Delete</button></td>`;
table.appendChild(row); table.appendChild(row);
}); });
const dirCount = document.getElementById('dirCount');
dirCount.textContent = data.length;
}) })
.catch(error => console.error('Error fetching directory:', error)); .catch(error => console.error('Error fetching directory:', error));
} }

View file

@ -0,0 +1,25 @@
const editForm = document.getElementById('editForm');
editForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(editForm);
const data = {};
for (const [key, value] of formData.entries()) {
data[key] = value;
}
const response = await fetch(`/api/v1/user/route`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
window.location.href = '/user';
} else {
alert('Failed to update entry');
}
});

View file

@ -6,12 +6,29 @@
<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">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/">AstroCom</a> <a class="navbar-brand" href="/">AstroCom</a>
<span id="footer"></span>
</div> </div>
<div class="ms-auto d-flex text-nowrap"> <div class="ms-auto d-flex text-nowrap">
<a href="/user" class="btn btn-outline-light me-2">User Login</a> <a href="/user" class="btn btn-outline-light me-2">User Login</a>
@ -40,6 +57,11 @@
<script src="/assets/js/bootstrap.min.js"></script> <script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<script>
$(function() {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>

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>

View file

@ -20,6 +20,22 @@
<script type="application/ld+json"> <script type="application/ld+json">
{"name":"AstroCom","description":"Simplifying communication.","@type":"WebSite","url":"https://astrocom.tel/","headline":"AstroCom","@context":"http://schema.org"} {"name":"AstroCom","description":"Simplifying communication.","@type":"WebSite","url":"https://astrocom.tel/","headline":"AstroCom","@context":"http://schema.org"}
</script> </script>
<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> <body>
@ -36,6 +52,8 @@
<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>

4
public/robots.txt Normal file
View file

@ -0,0 +1,4 @@
User-agent: *
Disallow: /
Allow: /$
Allow: /discord$

View file

@ -6,12 +6,29 @@
<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">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/">AstroCom</a> <a class="navbar-brand" href="/">AstroCom</a>
<span id="footer"></span>
</div> </div>
<div class="ms-auto d-flex text-nowrap"> <div class="ms-auto d-flex text-nowrap">
<a href="/user" class="btn btn-outline-light me-2">User Login</a> <a href="/user" class="btn btn-outline-light me-2">User Login</a>
@ -33,6 +50,47 @@
<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();
@ -57,6 +115,11 @@
<script src="/assets/js/bootstrap.min.js"></script> <script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<script>
$(function () {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>

View file

@ -0,0 +1,19 @@
#!/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\"}"

View file

@ -61,5 +61,11 @@
<script src="/assets/js/bootstrap.min.js"></script> <script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<div id="footer"></div>
<script>
$(function() {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>

View file

@ -10,6 +10,7 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">AstroCom</a> <a class="navbar-brand" href="#">AstroCom</a>
<span id="footer"></span>
<div class="navbar-nav ms-auto"> <div class="navbar-nav ms-auto">
<span class="navbar-text me-3"> <span class="navbar-text me-3">
Welcome, <%= user %> Welcome, <%= user %>
@ -41,5 +42,10 @@
<script src="/assets/js/bootstrap.min.js"></script> <script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<script>
$(function() {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>

View file

@ -1,15 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<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 Admin</title> <title>AstroCom Admin</title>
</head> </head>
<body class="bg-dark text-white"> <body class="bg-dark text-white">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">AstroCom</a> <a class="navbar-brand" href="#">AstroCom</a>
<span id="footer"></span>
<div class="navbar-nav ms-auto"> <div class="navbar-nav ms-auto">
<span class="navbar-text me-3"> <span class="navbar-text me-3">
Welcome, <%= user %> Welcome, <%= user %>
@ -18,57 +21,58 @@
</div> </div>
</div> </div>
</nav> </nav>
<div class="d-flex align-items-center gap-3 mb-3">
<h2 class="m-0">Admin Dashboard</h2>
<a href="/admin/create" class="btn btn-primary">Create New Server</a>
<p class="m-0">Total Servers: <span id="serverCount">0</span> | Total Directory Entries: <span
id="dirCount">0</span></p>
</div>
<div class="flex-grow-1 mb-3">
<table class="table table-striped table-dark" id="adminTable">
<thead>
<tr>
<th>ID</th>
<th>Hostname:Port</th>
<th>IAX Username/Context</th>
<th>IAX2 Secret</th>
<th>Number Block</th>
<th>API Key</th>
<th>Contact</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data will be dynamically populated by adminMain.js -->
</tbody>
</table>
</div>
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-3 mb-3"> <div class="d-flex align-items-center gap-3 mb-3">
<h2 class="m-0">Admin Dashboard</h2> <h2 class="m-0">Directory Management</h2>
<a href="/admin/create" class="btn btn-primary">Create New Server</a>
<p class="m-0">Total Servers: <span id="serverCount">0</span> | Total Directory Entries: <span id="dirCount">0</span></p>
</div>
<div class="d-flex">
<div class="flex-grow-1">
<table class="table table-striped table-dark" id="adminTable">
<thead>
<tr>
<th>ID</th>
<th>Hostname:Port</th>
<th>IAX Username/Context</th>
<th>IAX2 Secret</th>
<th>Number Block</th>
<th>API Key</th>
<th>Contact</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data will be dynamically populated by adminMain.js -->
</tbody>
</table>
</div>
<div class="d-flex">
<div class="flex-grow-1">
<!-- Original table is already in place above -->
</div>
<div class="ms-3 flex-grow-1">
<div class="d-flex align-items-center gap-3 mb-3">
<h2 class="m-0">Directory Management</h2>
</div>
<table class="table table-striped table-dark" id="directoryTable">
<thead>
<tr>
<th>Number</th>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="directoryList">
<!-- Data will be dynamically populated by adminMain.js -->
</tbody>
</table>
</div>
</div> </div>
<table class="table table-striped table-dark" id="directoryTable">
<thead>
<tr>
<th>Number</th>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="directoryList">
<!-- Data will be dynamically populated by adminMain.js -->
</tbody>
</table>
</div>
<script src="/assets/js/adminDirectory.js"></script> <script src="/assets/js/adminDirectory.js"></script>
<script src="/assets/js/adminMain.js"></script> <script src="/assets/js/adminMain.js"></script>
<script src="/assets/js/bootstrap.min.js"></script> <script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<script>
$(function () {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>

View file

@ -29,9 +29,14 @@
</div> </div>
</div> </div>
</div> </div>
<div id="footer" class="text-light mt-5"></div>
</div> </div>
<script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<script>
$(function() {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>

54
views/admin/register.ejs Normal file
View file

@ -0,0 +1,54 @@
<!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>

View file

21
views/footer.ejs Normal file
View file

@ -0,0 +1,21 @@
<footer class="footer mt-auto py-3 bg-dark text-white">
<div class="container text-center">
<span>&copy; <%= new Date().getFullYear() %> AstroCom <%= version %></span>
</div>
</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>

51
views/user/edit.ejs Normal file
View file

@ -0,0 +1,51 @@
<!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 User - Edit</title>
</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 class="navbar-nav ms-auto">
<span class="navbar-text me-3">
Welcome
</span>
<a href="/user/logout" class="btn btn-danger">Logout</a>
</div>
</div>
</nav>
<div class="container mt-4">
<h2>Edit Server Information</h2>
<form id="editForm" onsubmit="return false;" class="mt-4">
<% for (const [key, value] of Object.entries(data)) { %>
<% if (key !== 'id') { %>
<div class="mb-3">
<label for="<%= key %>" class="form-label"><%= key.charAt(0).toUpperCase() + key.slice(1) %></label>
<input type="text" class="form-control bg-dark text-white" id="<%= key %>" name="<%= key %>" value="<%= value %>">
</div>
<% } %>
<% } %>
<button type="submit" class="btn btn-primary">Update</button>
<a href="/user" class="btn btn-secondary">Cancel</a>
</form>
<script>
const route = <%= data.id %>;
</script>
</div>
<script src="/assets/js/userEdit.js"></script>
</div>
<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>

View file

@ -12,6 +12,7 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">AstroCom</a> <a class="navbar-brand" href="#">AstroCom</a>
<span id="footer"></span>
<div class="navbar-nav ms-auto"> <div class="navbar-nav ms-auto">
<span class="navbar-text me-3"> <span class="navbar-text me-3">
Welcome! Welcome!
@ -27,7 +28,7 @@
<div class="card bg-secondary mb-4 text-white"> <div class="card bg-secondary mb-4 text-white">
<div class="card-body"> <div class="card-body">
<div class="position-absolute top-0 end-0 m-3"> <div class="position-absolute top-0 end-0 m-3">
<a href="/user/edit" class="btn btn-primary disabled" id="editInfoBtn">Edit Information (Coming Soon)</a> <a href="/user/edit" class="btn btn-primary" id="editInfoBtn">Edit Information</a>
</div> </div>
<h5 class="card-title">Your Route Information</h5> <h5 class="card-title">Your Route Information</h5>
<p class="card-text"> <p class="card-text">
@ -72,6 +73,11 @@
<script src="/assets/js/bootstrap.min.js"></script> <script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<script>
$(function() {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>

View file

@ -18,16 +18,22 @@
<form action="/user/login" method="POST"> <form action="/user/login" method="POST">
<div class="mb-3"> <div class="mb-3">
<label for="apiKey" class="form-label">API Key:</label> <label for="apiKey" class="form-label">API Key:</label>
<input type="text" class="form-control" id="apiKey" name="apiKey" required> <input type="password" class="form-control" id="apiKey" name="apiKey" required>
</div> </div>
<button type="submit" class="btn btn-primary w-100">Login</button> <button type="submit" class="btn btn-primary w-100">Login</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div id="footer" class="text-light mt-5"></div>
</div> </div>
<script src="/assets/js/bootstrap.min.js"></script> <script src="/assets/js/bootstrap.min.js"></script>
<script src="/assets/js/bootstrap.bundle.min.js"></script> <script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/jquery.min.js"></script> <script src="/assets/js/jquery.min.js"></script>
<script>
$(function() {
$("#footer").load("/footer");
});
</script>
</body> </body>
</html> </html>