User panel is almost done!

This commit is contained in:
Christopher Cookman 2024-12-15 05:46:26 -07:00
parent cea5348d6c
commit 3c8dfe3a36
12 changed files with 525 additions and 17 deletions

208
index.js
View file

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

65
migrations.js Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -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 = `<td>${entry.number}</td><td>${entry.name}</td>`;
table.appendChild(row);
});
})
.catch(error => console.error('Error fetching directory:', error));
}
document.addEventListener('DOMContentLoaded', function () {
getDirectoryEntries();
});

View file

@ -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 = `<td>${entry.number}</td><td>${entry.name}</td><td><button onclick="deleteEntry(${entry.number})" class="btn btn-danger btn-sm">Delete</button></td>`;
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));
}

View file

@ -0,0 +1,41 @@
<!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 Directory</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>
</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="d-flex align-items-center gap-3 mb-3">
<h2 class="m-0 text-center w-100">AtroCom Directory</h2>
</div>
<div class="container mt-4" style="max-width: 400px;">
<table class="table table-dark table-striped">
<thead>
<tr>
<th>Number</th>
<th>Name</th>
</tr>
</thead>
<tbody id="directoryList"></tbody>
</table>
</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>
</body>
</html>

View file

@ -54,8 +54,6 @@
</form>
<script src="/assets/js/adminCreate.js"></script>
</div>
<!-- <script src="/assets/js/adminCreate.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>

0
views/directory.ejs Normal file
View file

77
views/user/index.ejs Normal file
View file

@ -0,0 +1,77 @@
<!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 Panel</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>
<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="d-flex align-items-center gap-3 mb-3">
<h2 class="m-0 text-center w-100">User Dashboard</h2>
</div>
<div class="container mt-4">
<div class="card bg-secondary mb-4 text-white">
<div class="card-body">
<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>
</div>
<h5 class="card-title">Your Route Information</h5>
<p class="card-text">
<strong>Route ID:</strong> <span id="routeId"></span><br>
<strong>Host:</strong> <span id="routeHost"></span><br>
<strong>IAX2 Username/context:</strong> <span id="routeAuth"></span><br>
<strong>IAX2 Password:</strong> <span id="routeSecret"></span><br>
<strong>Block:</strong> <span id="routeBlock"></span><br>
<strong>API Key:</strong> <span id="routeApiKey"></span>
</p>
</div>
</div>
</div>
<div class="container mt-4">
<h5 class="mb-3">Directory Entries</h5>
<table class="table table-dark table-striped">
<thead>
<tr>
<th>Number</th>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="directoryList">
<tr>
<form id="dirForm">
<td>
<input type="text" class="form-control" id="dirNumber" placeholder="Phone Number" required>
</td>
<td>
<input type="text" class="form-control" id="dirName" placeholder="Name" required>
</td>
<td>
<button type="submit" class="btn btn-primary" id="dirSubmit">Submit</button>
</td>
</form>
</tr>
</tbody>
</table>
</div>
<script src="/assets/js/userMain.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>
</body>
</html>

33
views/user/login.ejs Normal file
View file

@ -0,0 +1,33 @@
<!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 Login</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">User Login</h2>
<form action="/user/login" method="POST">
<div class="mb-3">
<label for="apiKey" class="form-label">API Key:</label>
<input type="text" class="form-control" id="apiKey" name="apiKey" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</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>
</body>
</html>