From 3c8dfe3a3612128d39553ac97b0748eb5dcc591f Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sun, 15 Dec 2024 05:46:26 -0700 Subject: [PATCH] User panel is almost done! --- index.js | 208 +++++++++++++++++++++++-- migrations.js | 65 ++++++++ migrations/001_gen_routes_table.sql | 10 ++ migrations/002_gen_users_table.sql | 5 + migrations/003_gen_directory_table.sql | 6 + public/assets/js/directory.js | 17 ++ public/assets/js/userMain.js | 78 ++++++++++ public/directory/index.html | 41 +++++ views/admin/create.ejs | 2 - views/directory.ejs | 0 views/user/index.ejs | 77 +++++++++ views/user/login.ejs | 33 ++++ 12 files changed, 525 insertions(+), 17 deletions(-) create mode 100644 migrations.js create mode 100644 migrations/001_gen_routes_table.sql create mode 100644 migrations/002_gen_users_table.sql create mode 100644 migrations/003_gen_directory_table.sql create mode 100644 public/assets/js/directory.js create mode 100644 public/assets/js/userMain.js create mode 100644 public/directory/index.html create mode 100644 views/directory.ejs create mode 100644 views/user/index.ejs create mode 100644 views/user/login.ejs diff --git a/index.js b/index.js index 88765e4..9492062 100644 --- a/index.js +++ b/index.js @@ -16,12 +16,10 @@ const db = new sqlite3.Database('astrocom.db', (err) => { console.log('Connected to SQLite database'); } }); +// Run migrations +require("./migrations")(db) -// Create 'routes' table -// We need to store server address, port, secret, block_start and block_length. Then make a query that takes in an arbitrary number, and returns a row if that number between block start and block start + block length. -db.run('CREATE TABLE IF NOT EXISTS routes (id INTEGER PRIMARY KEY AUTOINCREMENT, server TEXT NOT NULL, port INTEGER NOT NULL DEFAULT 4569, auth TEST NOT NULL DEFAULT \'from-astrocom\', secret TEXT NOT NULL, block_start INTEGER UNIQUE NOT NULL, block_length INTEGER NOT NULL DEFAULT 9999, apiKey TEXT NOT NULL)'); -db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, passwordHash TEXT NOT NULL)'); -// Check if user 1 exists, if not, create it (admin:admin) +// Check if user 1 exists, if not, create it const saltRounds = 10; db.get("SELECT * FROM users WHERE id = 1", [], (err, row) => { @@ -37,7 +35,7 @@ db.get("SELECT * FROM users WHERE id = 1", [], (err, row) => { return; } }); - // delete all users + // delete all users (The big scary one lol) db.run("DELETE FROM users", [], (err) => { if (err) { console.error('Error deleting users:', err); @@ -99,7 +97,7 @@ app.get('/admin/login', (req, res) => { }); app.get('/admin', (req, res) => { - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.redirect('/admin/login'); return; } @@ -107,7 +105,7 @@ app.get('/admin', (req, res) => { }); app.get('/admin/create', (req, res) => { - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.redirect('/admin/login'); return; } @@ -115,7 +113,7 @@ app.get('/admin/create', (req, res) => { }); app.get('/admin/route/:id', (req, res) => { - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.redirect('/admin/login'); return; } @@ -153,7 +151,7 @@ app.post('/admin/login', (req, res) => { return; } if (result) { - req.session.authenticated = true; + req.session.adminAuthenticated = true; req.session.user = row.username; res.redirect('/admin'); } else { @@ -164,7 +162,7 @@ app.post('/admin/login', (req, res) => { }) app.get('/api/v1/admin/routes', (req, res) => { // Get all routes - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.status(401).json({ error: 'Unauthorized' }); return; } @@ -179,7 +177,7 @@ app.get('/api/v1/admin/routes', (req, res) => { // Get all routes }); app.get('/api/v1/admin/route/:id', (req, res) => { // Get route - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.status(401).json({ error: 'Unauthorized' }); return; } @@ -198,7 +196,7 @@ app.get('/api/v1/admin/route/:id', (req, res) => { // Get route }); app.post('/api/v1/admin/route', (req, res) => { // Create a new route - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.status(401).json({ error: 'Unauthorized' }); return; } @@ -241,7 +239,7 @@ app.post('/api/v1/admin/route', (req, res) => { // Create a new route app.put('/api/v1/admin/route/:id', (req, res) => { // Update a route // Check if authenticated - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.status(401).json({ error: 'Unauthorized' }); return; } @@ -277,7 +275,7 @@ app.put('/api/v1/admin/route/:id', (req, res) => { // Update a route }); app.delete('/api/v1/admin/route/:id', (req, res) => { // Delete a route - if (!req.session.authenticated) { + if (!req.session.adminAuthenticated) { res.status(401).json({ error: 'Unauthorized' }); return; } @@ -293,6 +291,186 @@ app.delete('/api/v1/admin/route/:id', (req, res) => { // Delete a route // == END ADMIN ROUTES == +// == User routes == // allows someone to log in with their API key and add entries to the Directory (as long as the number is within their block range) +app.get('/user', (req, res) => { + if (!req.session.userAuthenticated) { + res.redirect('/user/login'); + return; + } + res.render('user/index', { user: req.session.user }); +}); + +app.get('/user/login', (req, res) => { + res.render('user/login'); +}); + +app.post('/user/login', (req, res) => { + const apiKey = req.body.apiKey; + db.get("SELECT * FROM routes WHERE apiKey = ?", [apiKey], (err, row) => { + if (err) { + console.error('Error getting route:', err); + res.status(500).send('Internal server error'); + return; + } + if (!row) { + res.status(401).send('Unauthorized'); + return; + } + req.session.userAuthenticated = true; + req.session.userData = row; + res.redirect('/user'); + }); +}); + +app.get('/user/logout', (req, res) => { + req.session.destroy(); + res.redirect('/user/login'); +}); + +app.get('/api/v1/user/route', (req, res) => { // Get route + if (!req.session.userAuthenticated) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + res.json(req.session.userData); +}); + +app.put('/api/v1/user/route', (req, res) => { // Update route + if (!req.session.userAuthenticated) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + if (!req.session.userData.apiKey) { + req.session.destroy(); // Something weird happened, destroy session + res.status(401).json({ error: 'Unauthorized' }); + return; + } + // Does not allow for ID to be specified, always update current users route + const server = req.body.server || req.session.userData.server; + const port = req.body.port || req.session.userData.port; + const auth = req.body.auth || req.session.userData.auth; + const secret = req.body.secret || req.session.userData.secret; + // We don't allow block changes, admins only. + const block_start = req.session.userData.block_start; + const block_length = req.session.userData.block_length; + const apiKey = req.session.userData.apiKey; + db.run('UPDATE routes SET server = ?, port = ?, auth = ?, secret = ?, block_start = ?, block_length = ? WHERE apiKey = ?', + [server, port, auth, secret, block_start, block_length, apiKey], + (err) => { + if (err) { + console.error('Error updating route:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + res.json({ message: 'Updated' }); + }); +}); + +app.get('/api/v1/user/directory', (req, res) => { // Get directory entries created by user + if (!req.session.userAuthenticated) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + db.all('SELECT * FROM directory WHERE route = ?', [req.session.userData.id], (err, rows) => { + if (err) { + console.error('Error getting routes:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + res.json(rows); + }); +}); + +app.post('/api/v1/user/directory', (req, res) => { // Create a new directory entry + // Check if authenticated + if (!req.session.userAuthenticated) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + // Check that the number is within the block range for the current user + var number = Number(req.body.number); + var name = String(req.body.name); + if (!number || !name) { + res.status(400).json({ error: 'Bad Request' }); + return; + } + + if (number < req.session.userData.block_start || number > req.session.userData.block_start + req.session.userData.block_length) { + res.status(403).json({ error: 'Forbidden' }); + return; + } + + // Remove html + name = name.replace(/<[^>]*>?/gm, ''); + const route = req.session.userData.id; + // If number already exists, update, otherwise insert + db.get('SELECT * FROM directory WHERE number = ? AND route = ?', [number, route], (err, row) => { + if (err) { + console.error('Error checking for existing directory entry:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + if (row) { + db.run('UPDATE directory SET name = ? WHERE number = ? AND route = ?', + [name, number, route], + (err) => { + if (err) { + console.error('Error updating directory entry:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + res.json({ message: 'Updated' }); + }); + } else { + db.run('INSERT INTO directory (number, name, route) VALUES (?, ?, ?)', + [number, name, route], + (err) => { + if (err) { + console.error('Error creating directory entry:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + res.status(201).json({ message: 'Created' }); + }); + } + }); +}); + +app.delete('/api/v1/user/directory/:number', (req, res) => { // Delete a directory entry + if (!req.session.userAuthenticated) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + const number = Number(req.params.number); + if (!number) { + res.status(400).json({ error: 'Bad Request' }); + return; + } + db.run('DELETE FROM directory WHERE number = ? AND route = ?', [number, req.session.userData.id], (err) => { + if (err) { + console.error('Error deleting directory entry:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + res.status(200).json({ message: 'Deleted' }); + }); +}); + +// == END USER ROUTES == + +// == Directory routes == (unauthenticated) + +app.get("/api/v1/directory", (req, res) => { + db.all("SELECT * FROM directory", (err, rows) => { + if (err) { + console.error('Error getting directory:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + res.json(rows); + }); +}); + // Query to get a route app.get('/api/v1/route/:apiKey/:ani/:number', (req, res) => { const apiKey = req.params.apiKey; diff --git a/migrations.js b/migrations.js new file mode 100644 index 0000000..32d1308 --- /dev/null +++ b/migrations.js @@ -0,0 +1,65 @@ +const sqlite3 = require('sqlite3').verbose(); +const fs = require('fs'); +const path = require('path'); +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)); + const getQuery = util.promisify(db.get.bind(db)); + + // Ensure a migrations table exists to track applied migrations + runQuery(`CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + 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) => { + return promise.then(() => { + const migrationName = path.basename(file); + + // Check if the migration has already been applied + return getQuery( + 'SELECT 1 FROM migrations WHERE name = ? LIMIT 1', + [migrationName] + ).then((row) => { + if (row) { + // console.log(`Skipping already applied migration: ${migrationName}`); + return; // Skip this migration + } + + // Read and execute the migration SQL + const migrationPath = path.join(migrationDir, file); + const sql = fs.readFileSync(migrationPath, 'utf8'); + + return runQuery(sql).then(() => { + // Record the applied migration + return runQuery( + 'INSERT INTO migrations (name) VALUES (?)', + [migrationName] + ).then(() => { + console.log(`Applied migration: ${migrationName}`); + }); + }); + }); + }); + }, Promise.resolve()); + }) + .then(() => { + console.log('All migrations applied successfully!'); + resolve(); + }) + .catch((err) => { + console.error('Error running migrations:', err); + reject(err); + }) + }); +} + +module.exports = runMigrations; diff --git a/migrations/001_gen_routes_table.sql b/migrations/001_gen_routes_table.sql new file mode 100644 index 0000000..7d96a5f --- /dev/null +++ b/migrations/001_gen_routes_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS routes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + server TEXT NOT NULL, + port INTEGER NOT NULL DEFAULT 4569, + auth TEST NOT NULL DEFAULT 'from-astrocom', + secret TEXT NOT NULL, + block_start INTEGER UNIQUE NOT NULL, + block_length INTEGER NOT NULL DEFAULT 9999, + apiKey TEXT NOT NULL +) \ No newline at end of file diff --git a/migrations/002_gen_users_table.sql b/migrations/002_gen_users_table.sql new file mode 100644 index 0000000..a91dfb3 --- /dev/null +++ b/migrations/002_gen_users_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + passwordHash TEXT NOT NULL +) \ No newline at end of file diff --git a/migrations/003_gen_directory_table.sql b/migrations/003_gen_directory_table.sql new file mode 100644 index 0000000..24ba762 --- /dev/null +++ b/migrations/003_gen_directory_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS directory ( + 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. + 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) +) \ No newline at end of file diff --git a/public/assets/js/directory.js b/public/assets/js/directory.js new file mode 100644 index 0000000..0c768fb --- /dev/null +++ b/public/assets/js/directory.js @@ -0,0 +1,17 @@ +function getDirectoryEntries() { + fetch('/api/v1/directory') + .then(response => response.json()) + .then(data => { + const table = document.getElementById('directoryList'); + data.forEach(entry => { + const row = document.createElement('tr'); + row.innerHTML = `${entry.number}${entry.name}`; + table.appendChild(row); + }); + }) + .catch(error => console.error('Error fetching directory:', error)); +} + +document.addEventListener('DOMContentLoaded', function () { + getDirectoryEntries(); +}); \ No newline at end of file diff --git a/public/assets/js/userMain.js b/public/assets/js/userMain.js new file mode 100644 index 0000000..ff17a41 --- /dev/null +++ b/public/assets/js/userMain.js @@ -0,0 +1,78 @@ +document.addEventListener('DOMContentLoaded', function () { + getInfo(); + getDirectoryEntries(); +}); + +async function getInfo() { + try { + const response = await fetch('/api/v1/user/route'); + const route = await response.json(); + document.getElementById('routeId').textContent = route.id || ''; + document.getElementById('routeHost').textContent = `${route.server}:${route.port}` || ''; + document.getElementById('routeAuth').textContent = route.auth || ''; + document.getElementById('routeSecret').textContent = route.secret || ''; + document.getElementById('routeBlock').textContent = `${route.block_start} - ${route.block_start + route.block_length}` || ''; + document.getElementById('routeApiKey').textContent = route.apiKey || ''; + } catch (error) { + console.error('Error fetching route info:', error); + } +} + +// Get directory entries +function getDirectoryEntries() { + fetch('/api/v1/user/directory') + .then(response => response.json()) + .then(data => { + const table = document.getElementById('directoryList'); + // Keep the first row (update form) and remove the rest + while (table.children.length > 1) { + table.removeChild(table.lastChild); + } + data.forEach(entry => { + const row = document.createElement('tr'); + row.innerHTML = `${entry.number}${entry.name}`; + table.appendChild(row); + }); + }) + .catch(error => console.error('Error fetching directory:', error)); +} + + +// Handle update form for directory entries +document.getElementById('dirForm').addEventListener('submit', function(event) { + event.preventDefault(); + const number = document.getElementById('dirNumber').value; + const name = document.getElementById('dirName').value; + const submitButton = document.getElementById('dirSubmit'); + + submitButton.disabled = true; + fetch('/api/v1/user/directory', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ number, name }) + }) + .then(response => response.json()) + .then(data => { + getDirectoryEntries(); + submitButton.disabled = false; + }) + .catch(error => { + console.error('Error updating directory:', error); + submitButton.disabled = false; + }); +}); + +// Handle delete entry +function deleteEntry(number) { + fetch(`/api/v1/user/directory/${number}`, { + method: 'DELETE' + }) + .then(response => { + if (response.status === 200) { + getDirectoryEntries(); + } + }) + .catch(error => console.error('Error deleting entry:', error)); +} \ No newline at end of file diff --git a/public/directory/index.html b/public/directory/index.html new file mode 100644 index 0000000..0f59b27 --- /dev/null +++ b/public/directory/index.html @@ -0,0 +1,41 @@ + + + + + + + + AstroCom Directory + + + + +
+

AtroCom Directory

+
+
+ + + + + + + + +
NumberName
+
+ + + + + + + \ No newline at end of file diff --git a/views/admin/create.ejs b/views/admin/create.ejs index e88f273..5e5dac2 100644 --- a/views/admin/create.ejs +++ b/views/admin/create.ejs @@ -54,8 +54,6 @@ - - diff --git a/views/directory.ejs b/views/directory.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/user/index.ejs b/views/user/index.ejs new file mode 100644 index 0000000..0b816ef --- /dev/null +++ b/views/user/index.ejs @@ -0,0 +1,77 @@ + + + + + + + + AstroCom User Panel + + + + +
+

User Dashboard

+
+
+
+
+ +
Your Route Information
+

+ Route ID:
+ Host:
+ IAX2 Username/context:
+ IAX2 Password:
+ Block:
+ API Key: +

+
+
+
+
+
Directory Entries
+ + + + + + + + + + + + + + + + + +
NumberNameActions
+ + + + + +
+
+ + + + + + + \ No newline at end of file diff --git a/views/user/login.ejs b/views/user/login.ejs new file mode 100644 index 0000000..9fd40f8 --- /dev/null +++ b/views/user/login.ejs @@ -0,0 +1,33 @@ + + + + + + + + AstroCom User Login + + + +
+
+
+
+
+

User Login

+
+
+ + +
+ +
+
+
+
+
+ + + + + \ No newline at end of file