Finalized ssh tunnels, updatetd database schemas, started on config editor.

This commit is contained in:
LukeGus
2025-07-21 01:25:38 -05:00
parent cfaa04e42c
commit 547701378f
18 changed files with 4791 additions and 25 deletions

View File

@@ -2,6 +2,7 @@ import express from 'express';
import bodyParser from 'body-parser';
import userRoutes from './routes/users.js';
import sshRoutes from './routes/ssh.js';
import sshTunnelRoutes from './routes/ssh_tunnel.js';
import chalk from 'chalk';
import cors from 'cors';
@@ -47,6 +48,7 @@ app.get('/health', (req, res) => {
app.use('/users', userRoutes);
app.use('/ssh', sshRoutes);
app.use('/ssh_tunnel', sshTunnelRoutes);
app.use((err: unknown, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error('Unhandled error:', err);

View File

@@ -64,6 +64,36 @@ CREATE TABLE IF NOT EXISTS ssh_data (
is_pinned INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS ssh_tunnel_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT,
folder TEXT,
source_port INTEGER NOT NULL,
endpoint_port INTEGER NOT NULL,
source_ip TEXT NOT NULL,
source_ssh_port INTEGER NOT NULL,
source_username TEXT,
source_password TEXT,
source_auth_method TEXT,
source_ssh_key TEXT,
source_key_password TEXT,
source_key_type TEXT,
endpoint_ip TEXT NOT NULL,
endpoint_ssh_port INTEGER NOT NULL,
endpoint_username TEXT,
endpoint_password TEXT,
endpoint_auth_method TEXT,
endpoint_ssh_key TEXT,
endpoint_key_password TEXT,
endpoint_key_type TEXT,
max_retries INTEGER NOT NULL DEFAULT 3,
retry_interval INTEGER NOT NULL DEFAULT 5000,
connection_state TEXT NOT NULL DEFAULT 'DISCONNECTED',
auto_start INTEGER NOT NULL DEFAULT 0,
is_pinned INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL

View File

@@ -25,6 +25,36 @@ export const sshData = sqliteTable('ssh_data', {
isPinned: integer('is_pinned', { mode: 'boolean' }),
});
export const sshTunnelData = sqliteTable('ssh_tunnel_data', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: text('user_id').notNull().references(() => users.id),
name: text('name'),
folder: text('folder'),
sourcePort: integer('source_port').notNull(),
endpointPort: integer('endpoint_port').notNull(),
sourceIP: text('source_ip').notNull(),
sourceSSHPort: integer('source_ssh_port').notNull(),
sourceUsername: text('source_username'),
sourcePassword: text('source_password'),
sourceAuthMethod: text('source_auth_method'),
sourceSSHKey: text('source_ssh_key', { length: 8192 }),
sourceKeyPassword: text('source_key_password'),
sourceKeyType: text('source_key_type'),
endpointIP: text('endpoint_ip').notNull(),
endpointSSHPort: integer('endpoint_ssh_port').notNull(),
endpointUsername: text('endpoint_username'),
endpointPassword: text('endpoint_password'),
endpointAuthMethod: text('endpoint_auth_method'),
endpointSSHKey: text('endpoint_ssh_key', { length: 8192 }),
endpointKeyPassword: text('endpoint_key_password'),
endpointKeyType: text('endpoint_key_type'),
maxRetries: integer('max_retries').notNull().default(3),
retryInterval: integer('retry_interval').notNull().default(5000),
connectionState: text('connection_state').notNull().default('DISCONNECTED'),
autoStart: integer('auto_start', { mode: 'boolean' }).notNull().default(false),
isPinned: integer('is_pinned', { mode: 'boolean' }).notNull().default(false),
});
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value').notNull(),

View File

@@ -0,0 +1,306 @@
import express from 'express';
import { db } from '../db/index.js';
import { sshTunnelData } from '../db/schema.js';
import { eq, and } from 'drizzle-orm';
import chalk from 'chalk';
import jwt from 'jsonwebtoken';
import type { Request, Response, NextFunction } from 'express';
const dbIconSymbol = '🗄️';
const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`);
const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => {
return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`;
};
const logger = {
info: (msg: string): void => {
console.log(formatMessage('info', chalk.cyan, msg));
},
warn: (msg: string): void => {
console.warn(formatMessage('warn', chalk.yellow, msg));
},
error: (msg: string, err?: unknown): void => {
console.error(formatMessage('error', chalk.redBright, msg));
if (err) console.error(err);
},
success: (msg: string): void => {
console.log(formatMessage('success', chalk.greenBright, msg));
},
debug: (msg: string): void => {
if (process.env.NODE_ENV !== 'production') {
console.debug(formatMessage('debug', chalk.magenta, msg));
}
}
};
const router = express.Router();
function isNonEmptyString(val: any): val is string {
return typeof val === 'string' && val.trim().length > 0;
}
function isValidPort(val: any): val is number {
return typeof val === 'number' && val > 0 && val < 65536;
}
interface JWTPayload {
userId: string;
iat?: number;
exp?: number;
}
// JWT authentication middleware
function authenticateJWT(req: Request, res: Response, next: NextFunction) {
// Only allow bypass if X-Internal-Request header is set
if (req.headers['x-internal-request'] === '1') {
(req as any).userId = 'internal_service';
return next();
}
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
logger.warn('Missing or invalid Authorization header');
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const token = authHeader.split(' ')[1];
const jwtSecret = process.env.JWT_SECRET || 'secret';
try {
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
(req as any).userId = payload.userId;
next();
} catch (err) {
logger.warn('Invalid or expired token');
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Route: Create SSH tunnel data (requires JWT)
// POST /ssh_tunnel/tunnel
router.post('/tunnel', authenticateJWT, async (req: Request, res: Response) => {
const {
name, folder, sourcePort, endpointPort, sourceIP, sourceSSHPort, sourceUsername,
sourcePassword, sourceAuthMethod, sourceSSHKey, sourceKeyPassword, sourceKeyType,
endpointIP, endpointSSHPort, endpointUsername, endpointPassword, endpointAuthMethod,
endpointSSHKey, endpointKeyPassword, endpointKeyType, maxRetries, retryInterval, autoStart, isPinned
} = req.body;
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || !isNonEmptyString(sourceIP) || !isValidPort(sourcePort) ||
!isValidPort(endpointPort) || !isValidPort(sourceSSHPort) || !isNonEmptyString(endpointIP) ||
!isValidPort(endpointSSHPort)) {
logger.warn('Invalid SSH tunnel data input');
return res.status(400).json({ error: 'Invalid SSH tunnel data' });
}
const sshTunnelDataObj: any = {
userId: userId,
name,
folder,
sourcePort,
endpointPort,
sourceIP,
sourceSSHPort,
sourceUsername,
sourceAuthMethod,
endpointIP,
endpointSSHPort,
endpointUsername,
endpointAuthMethod,
maxRetries: maxRetries || 3,
retryInterval: retryInterval || 5000,
connectionState: 'DISCONNECTED',
autoStart: autoStart || false,
isPinned: isPinned || false
};
// Handle source authentication
if (sourceAuthMethod === 'password') {
sshTunnelDataObj.sourcePassword = sourcePassword;
sshTunnelDataObj.sourceSSHKey = null;
sshTunnelDataObj.sourceKeyPassword = null;
sshTunnelDataObj.sourceKeyType = null;
} else if (sourceAuthMethod === 'key') {
sshTunnelDataObj.sourceSSHKey = sourceSSHKey;
sshTunnelDataObj.sourceKeyPassword = sourceKeyPassword;
sshTunnelDataObj.sourceKeyType = sourceKeyType;
sshTunnelDataObj.sourcePassword = null;
}
// Handle endpoint authentication
if (endpointAuthMethod === 'password') {
sshTunnelDataObj.endpointPassword = endpointPassword;
sshTunnelDataObj.endpointSSHKey = null;
sshTunnelDataObj.endpointKeyPassword = null;
sshTunnelDataObj.endpointKeyType = null;
} else if (endpointAuthMethod === 'key') {
sshTunnelDataObj.endpointSSHKey = endpointSSHKey;
sshTunnelDataObj.endpointKeyPassword = endpointKeyPassword;
sshTunnelDataObj.endpointKeyType = endpointKeyType;
sshTunnelDataObj.endpointPassword = null;
}
try {
await db.insert(sshTunnelData).values(sshTunnelDataObj);
res.json({ message: 'SSH tunnel data created' });
} catch (err) {
logger.error('Failed to save SSH tunnel data', err);
res.status(500).json({ error: 'Failed to save SSH tunnel data' });
}
});
// Route: Update SSH tunnel data (requires JWT)
// PUT /ssh_tunnel/tunnel/:id
router.put('/tunnel/:id', authenticateJWT, async (req: Request, res: Response) => {
const {
name, folder, sourcePort, endpointPort, sourceIP, sourceSSHPort, sourceUsername,
sourcePassword, sourceAuthMethod, sourceSSHKey, sourceKeyPassword, sourceKeyType,
endpointIP, endpointSSHPort, endpointUsername, endpointPassword, endpointAuthMethod,
endpointSSHKey, endpointKeyPassword, endpointKeyType, maxRetries, retryInterval, autoStart, isPinned
} = req.body;
const { id } = req.params;
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || !isNonEmptyString(sourceIP) || !isValidPort(sourcePort) ||
!isValidPort(endpointPort) || !isValidPort(sourceSSHPort) || !isNonEmptyString(endpointIP) ||
!isValidPort(endpointSSHPort) || !id) {
logger.warn('Invalid SSH tunnel data input for update');
return res.status(400).json({ error: 'Invalid SSH tunnel data' });
}
const sshTunnelDataObj: any = {
name,
folder,
sourcePort,
endpointPort,
sourceIP,
sourceSSHPort,
sourceUsername,
sourceAuthMethod,
endpointIP,
endpointSSHPort,
endpointUsername,
endpointAuthMethod,
maxRetries: maxRetries || 3,
retryInterval: retryInterval || 5000,
autoStart: autoStart || false,
isPinned: isPinned || false
};
// Handle source authentication
if (sourceAuthMethod === 'password') {
sshTunnelDataObj.sourcePassword = sourcePassword;
sshTunnelDataObj.sourceSSHKey = null;
sshTunnelDataObj.sourceKeyPassword = null;
sshTunnelDataObj.sourceKeyType = null;
} else if (sourceAuthMethod === 'key') {
sshTunnelDataObj.sourceSSHKey = sourceSSHKey;
sshTunnelDataObj.sourceKeyPassword = sourceKeyPassword;
sshTunnelDataObj.sourceKeyType = sourceKeyType;
sshTunnelDataObj.sourcePassword = null;
}
// Handle endpoint authentication
if (endpointAuthMethod === 'password') {
sshTunnelDataObj.endpointPassword = endpointPassword;
sshTunnelDataObj.endpointSSHKey = null;
sshTunnelDataObj.endpointKeyPassword = null;
sshTunnelDataObj.endpointKeyType = null;
} else if (endpointAuthMethod === 'key') {
sshTunnelDataObj.endpointSSHKey = endpointSSHKey;
sshTunnelDataObj.endpointKeyPassword = endpointKeyPassword;
sshTunnelDataObj.endpointKeyType = endpointKeyType;
sshTunnelDataObj.endpointPassword = null;
}
try {
const result = await db.update(sshTunnelData)
.set(sshTunnelDataObj)
.where(and(eq(sshTunnelData.id, Number(id)), eq(sshTunnelData.userId, userId)));
res.json({ message: 'SSH tunnel data updated' });
} catch (err) {
logger.error('Failed to update SSH tunnel data', err);
res.status(500).json({ error: 'Failed to update SSH tunnel data' });
}
});
// Route: Get SSH tunnel data for the authenticated user (requires JWT)
// GET /ssh_tunnel/tunnel
router.get('/tunnel', authenticateJWT, async (req: Request, res: Response) => {
// If internal request and allAutoStart=1, return all autoStart tunnels
if (req.headers['x-internal-request'] === '1' && req.query.allAutoStart === '1') {
try {
const data = await db
.select()
.from(sshTunnelData)
.where(eq(sshTunnelData.autoStart, true));
return res.json(data);
} catch (err) {
logger.error('Failed to fetch all auto-start SSH tunnel data', err);
return res.status(500).json({ error: 'Failed to fetch auto-start SSH tunnel data' });
}
}
// Default: filter by userId
const userId = (req as any).userId;
if (!isNonEmptyString(userId)) {
logger.warn('Invalid userId for SSH tunnel data fetch');
return res.status(400).json({ error: 'Invalid userId' });
}
try {
const data = await db
.select()
.from(sshTunnelData)
.where(eq(sshTunnelData.userId, userId));
res.json(data);
} catch (err) {
logger.error('Failed to fetch SSH tunnel data', err);
res.status(500).json({ error: 'Failed to fetch SSH tunnel data' });
}
});
// Route: Get all unique folders for the authenticated user (requires JWT)
// GET /ssh_tunnel/folders
router.get('/folders', authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId;
if (!isNonEmptyString(userId)) {
logger.warn('Invalid userId for SSH tunnel folder fetch');
return res.status(400).json({ error: 'Invalid userId' });
}
try {
const data = await db
.select({ folder: sshTunnelData.folder })
.from(sshTunnelData)
.where(eq(sshTunnelData.userId, userId));
const folderCounts: Record<string, number> = {};
data.forEach(d => {
if (d.folder && d.folder.trim() !== '') {
folderCounts[d.folder] = (folderCounts[d.folder] || 0) + 1;
}
});
const folders = Object.keys(folderCounts).filter(folder => folderCounts[folder] > 0);
res.json(folders);
} catch (err) {
logger.error('Failed to fetch SSH tunnel folders', err);
res.status(500).json({ error: 'Failed to fetch SSH tunnel folders' });
}
});
// Route: Delete SSH tunnel by id (requires JWT)
// DELETE /ssh_tunnel/tunnel/:id
router.delete('/tunnel/:id', authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId;
const { id } = req.params;
if (!isNonEmptyString(userId) || !id) {
logger.warn('Invalid userId or id for SSH tunnel delete');
return res.status(400).json({ error: 'Invalid userId or id' });
}
try {
const result = await db.delete(sshTunnelData)
.where(and(eq(sshTunnelData.id, Number(id)), eq(sshTunnelData.userId, userId)));
res.json({ message: 'SSH tunnel deleted' });
} catch (err) {
logger.error('Failed to delete SSH tunnel', err);
res.status(500).json({ error: 'Failed to delete SSH tunnel' });
}
});
export default router;

View File

@@ -133,7 +133,6 @@ router.post('/get', async (req, res) => {
}
const jwtSecret = process.env.JWT_SECRET || 'secret';
const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { expiresIn: '50d' });
logger.success(`User authenticated: ${username}`);
res.json({ token });
} catch (err) {
logger.error('Failed to get user', err);

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
import './db/database.js'
import './ssh/ssh.js';
import './ssh_tunnel/ssh_tunnel.js';
import chalk from 'chalk';
const fixedIconSymbol = '🚀';