Improve logging frontend/backend, fix host form being reversed.
This commit is contained in:
@@ -35,23 +35,20 @@ class GitHubCache {
|
||||
timestamp: now,
|
||||
expiresAt: now + this.CACHE_DURATION
|
||||
});
|
||||
databaseLogger.debug(`Cache entry set`, { operation: 'cache_set', key, expiresIn: this.CACHE_DURATION });
|
||||
// Cache entry set
|
||||
}
|
||||
|
||||
get(key: string): any | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) {
|
||||
databaseLogger.debug(`Cache miss`, { operation: 'cache_get', key });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
databaseLogger.debug(`Cache entry expired`, { operation: 'cache_get', key, expired: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
databaseLogger.debug(`Cache hit`, { operation: 'cache_get', key, age: Date.now() - entry.timestamp });
|
||||
return entry.data;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +80,6 @@ interface GitHubRelease {
|
||||
async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise<any> {
|
||||
const cachedData = githubCache.get(cacheKey);
|
||||
if (cachedData) {
|
||||
databaseLogger.debug(`Using cached GitHub API data`, { operation: 'github_api', endpoint, cached: true });
|
||||
return {
|
||||
data: cachedData,
|
||||
cached: true,
|
||||
@@ -92,7 +88,6 @@ async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise<any>
|
||||
}
|
||||
|
||||
try {
|
||||
databaseLogger.info(`Fetching from GitHub API`, { operation: 'github_api', endpoint });
|
||||
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
@@ -108,7 +103,6 @@ async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise<any>
|
||||
const data = await response.json();
|
||||
githubCache.set(cacheKey, data);
|
||||
|
||||
databaseLogger.success(`GitHub API data fetched successfully`, { operation: 'github_api', endpoint, dataSize: JSON.stringify(data).length });
|
||||
return {
|
||||
data: data,
|
||||
cached: false
|
||||
@@ -122,12 +116,10 @@ async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise<any>
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
apiLogger.info(`Health check requested`, { operation: 'health_check' });
|
||||
res.json({status: 'ok'});
|
||||
});
|
||||
|
||||
app.get('/version', async (req, res) => {
|
||||
apiLogger.info(`Version check requested`, { operation: 'version_check' });
|
||||
let localVersion = process.env.VERSION;
|
||||
|
||||
if (!localVersion) {
|
||||
@@ -135,7 +127,6 @@ app.get('/version', async (req, res) => {
|
||||
const packagePath = path.resolve(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
localVersion = packageJson.version;
|
||||
databaseLogger.debug(`Version read from package.json`, { operation: 'version_check', localVersion });
|
||||
} catch (error) {
|
||||
databaseLogger.error('Failed to read version from package.json', error, { operation: 'version_check' });
|
||||
}
|
||||
@@ -163,13 +154,6 @@ app.get('/version', async (req, res) => {
|
||||
}
|
||||
|
||||
const isUpToDate = localVersion === remoteVersion;
|
||||
databaseLogger.info(`Version comparison completed`, {
|
||||
operation: 'version_check',
|
||||
localVersion,
|
||||
remoteVersion,
|
||||
isUpToDate,
|
||||
cached: releaseData.cached
|
||||
});
|
||||
|
||||
const response = {
|
||||
status: isUpToDate ? 'up_to_date' : 'requires_update',
|
||||
@@ -198,7 +182,7 @@ app.get('/releases/rss', async (req, res) => {
|
||||
const per_page = Math.min(parseInt(req.query.per_page as string) || 20, 100);
|
||||
const cacheKey = `releases_rss_${page}_${per_page}`;
|
||||
|
||||
apiLogger.info(`RSS releases requested`, { operation: 'rss_releases', page, per_page });
|
||||
// RSS releases requested
|
||||
|
||||
const releasesData = await fetchGitHubAPI(
|
||||
`/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`,
|
||||
@@ -235,13 +219,7 @@ app.get('/releases/rss', async (req, res) => {
|
||||
cache_age: releasesData.cache_age
|
||||
};
|
||||
|
||||
databaseLogger.success(`RSS releases generated successfully`, {
|
||||
operation: 'rss_releases',
|
||||
itemCount: rssItems.length,
|
||||
page,
|
||||
per_page,
|
||||
cached: releasesData.cached
|
||||
});
|
||||
// RSS releases generated successfully
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
|
||||
@@ -165,13 +165,6 @@ const migrateSchema = () => {
|
||||
addColumnIfNotExists('users', 'issuer_url', 'TEXT');
|
||||
addColumnIfNotExists('users', 'authorization_url', 'TEXT');
|
||||
addColumnIfNotExists('users', 'token_url', 'TEXT');
|
||||
try {
|
||||
databaseLogger.debug('Attempting to drop redirect_uri column', { operation: 'schema_migration', table: 'users' });
|
||||
sqlite.prepare(`ALTER TABLE users DROP COLUMN redirect_uri`).run();
|
||||
databaseLogger.success('redirect_uri column dropped', { operation: 'schema_migration', table: 'users' });
|
||||
} catch (e) {
|
||||
databaseLogger.debug('redirect_uri column does not exist or could not be dropped', { operation: 'schema_migration', table: 'users' });
|
||||
}
|
||||
|
||||
addColumnIfNotExists('users', 'identifier_path', 'TEXT');
|
||||
addColumnIfNotExists('users', 'name_path', 'TEXT');
|
||||
|
||||
@@ -63,11 +63,8 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
||||
const cacheKey = 'termix_alerts';
|
||||
const cachedData = alertCache.get(cacheKey);
|
||||
if (cachedData) {
|
||||
authLogger.info('Returning cached alerts from GitHub', { operation: 'alerts_fetch', cacheKey, alertCount: cachedData.length });
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
authLogger.info('Fetching alerts from GitHub', { operation: 'alerts_fetch', url: `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${ALERTS_FILE}` });
|
||||
try {
|
||||
const url = `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${ALERTS_FILE}`;
|
||||
|
||||
@@ -84,7 +81,6 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
||||
}
|
||||
|
||||
const alerts: TermixAlert[] = await response.json() as TermixAlert[];
|
||||
authLogger.info('Successfully fetched alerts from GitHub', { operation: 'alerts_fetch', totalAlerts: alerts.length });
|
||||
|
||||
const now = new Date();
|
||||
|
||||
@@ -94,9 +90,7 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
|
||||
return isValid;
|
||||
});
|
||||
|
||||
authLogger.info('Filtered alerts by expiry date', { operation: 'alerts_fetch', totalAlerts: alerts.length, validAlerts: validAlerts.length });
|
||||
alertCache.set(cacheKey, validAlerts);
|
||||
authLogger.success('Alerts cached successfully', { operation: 'alerts_fetch', alertCount: validAlerts.length });
|
||||
return validAlerts;
|
||||
} catch (error) {
|
||||
authLogger.error('Failed to fetch alerts from GitHub', { operation: 'alerts_fetch', error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
|
||||
@@ -76,16 +76,12 @@ router.get('/db/host/internal', async (req: Request, res: Response) => {
|
||||
// POST /ssh/host
|
||||
router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
sshLogger.info('SSH host creation request received', { operation: 'host_create', userId, contentType: req.headers['content-type'] });
|
||||
|
||||
let hostData: any;
|
||||
|
||||
if (req.headers['content-type']?.includes('multipart/form-data')) {
|
||||
sshLogger.info('Processing multipart form data for SSH host creation', { operation: 'host_create', userId });
|
||||
if (req.body.data) {
|
||||
try {
|
||||
hostData = JSON.parse(req.body.data);
|
||||
sshLogger.info('Successfully parsed JSON data from multipart request', { operation: 'host_create', userId, hasKey: !!req.file });
|
||||
} catch (err) {
|
||||
sshLogger.warn('Invalid JSON data in multipart request', { operation: 'host_create', userId, error: err });
|
||||
return res.status(400).json({error: 'Invalid JSON data'});
|
||||
@@ -97,11 +93,9 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque
|
||||
|
||||
if (req.file) {
|
||||
hostData.key = req.file.buffer.toString('utf8');
|
||||
sshLogger.info('SSH key file processed from multipart request', { operation: 'host_create', userId, keySize: req.file.size });
|
||||
}
|
||||
} else {
|
||||
hostData = req.body;
|
||||
sshLogger.info('Processing JSON data for SSH host creation', { operation: 'host_create', userId });
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -147,11 +141,11 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque
|
||||
username,
|
||||
authType: effectiveAuthType,
|
||||
credentialId: credentialId || null,
|
||||
pin: !pin ? 1 : 0,
|
||||
enableTerminal: !enableTerminal ? 1 : 0,
|
||||
enableTunnel: !enableTunnel ? 1 : 0,
|
||||
pin: pin ? 1 : 0,
|
||||
enableTerminal: enableTerminal ? 1 : 0,
|
||||
enableTunnel: enableTunnel ? 1 : 0,
|
||||
tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) : null,
|
||||
enableFileManager: !!enableFileManager ? 1 : 0,
|
||||
enableFileManager: enableFileManager ? 1 : 0,
|
||||
defaultPath: defaultPath || null,
|
||||
};
|
||||
|
||||
@@ -160,20 +154,35 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.keyType = null;
|
||||
sshLogger.info('SSH host configured for password authentication', { operation: 'host_create', userId, name, ip, port });
|
||||
} else if (effectiveAuthType === 'key') {
|
||||
sshDataObj.key = key || null;
|
||||
sshDataObj.keyPassword = keyPassword || null;
|
||||
sshDataObj.keyType = keyType;
|
||||
sshDataObj.password = null;
|
||||
sshLogger.info('SSH host configured for key authentication', { operation: 'host_create', userId, name, ip, port, keyType });
|
||||
}
|
||||
|
||||
try {
|
||||
sshLogger.info('Attempting to save SSH host to database', { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType });
|
||||
await db.insert(sshData).values(sshDataObj);
|
||||
sshLogger.success('SSH host created successfully', { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType, enableTerminal, enableTunnel, enableFileManager });
|
||||
res.json({message: 'SSH data created'});
|
||||
const result = await db.insert(sshData).values(sshDataObj).returning();
|
||||
|
||||
if (result.length === 0) {
|
||||
sshLogger.warn('No host returned after creation', { operation: 'host_create', userId, name, ip, port });
|
||||
return res.status(500).json({error: 'Failed to create host'});
|
||||
}
|
||||
|
||||
const createdHost = result[0];
|
||||
const baseHost = {
|
||||
...createdHost,
|
||||
tags: typeof createdHost.tags === 'string' ? (createdHost.tags ? createdHost.tags.split(',').filter(Boolean) : []) : [],
|
||||
pin: !!createdHost.pin,
|
||||
enableTerminal: !!createdHost.enableTerminal,
|
||||
enableTunnel: !!createdHost.enableTunnel,
|
||||
tunnelConnections: createdHost.tunnelConnections ? JSON.parse(createdHost.tunnelConnections) : [],
|
||||
enableFileManager: !!createdHost.enableFileManager,
|
||||
};
|
||||
|
||||
const resolvedHost = await resolveHostCredentials(baseHost) || baseHost;
|
||||
|
||||
res.json(resolvedHost);
|
||||
} catch (err) {
|
||||
sshLogger.error('Failed to save SSH host to database', err, { operation: 'host_create', userId, name, ip, port, authType: effectiveAuthType });
|
||||
res.status(500).json({error: 'Failed to save SSH data'});
|
||||
@@ -185,16 +194,12 @@ router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Reque
|
||||
router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Request, res: Response) => {
|
||||
const hostId = req.params.id;
|
||||
const userId = (req as any).userId;
|
||||
sshLogger.info('SSH host update request received', { operation: 'host_update', hostId: parseInt(hostId), userId, contentType: req.headers['content-type'] });
|
||||
|
||||
let hostData: any;
|
||||
|
||||
if (req.headers['content-type']?.includes('multipart/form-data')) {
|
||||
sshLogger.info('Processing multipart form data for SSH host update', { operation: 'host_update', hostId: parseInt(hostId), userId });
|
||||
if (req.body.data) {
|
||||
try {
|
||||
hostData = JSON.parse(req.body.data);
|
||||
sshLogger.info('Successfully parsed JSON data from multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId, hasKey: !!req.file });
|
||||
} catch (err) {
|
||||
sshLogger.warn('Invalid JSON data in multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId, error: err });
|
||||
return res.status(400).json({error: 'Invalid JSON data'});
|
||||
@@ -206,11 +211,9 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
||||
|
||||
if (req.file) {
|
||||
hostData.key = req.file.buffer.toString('utf8');
|
||||
sshLogger.info('SSH key file processed from multipart request', { operation: 'host_update', hostId: parseInt(hostId), userId, keySize: req.file.size });
|
||||
}
|
||||
} else {
|
||||
hostData = req.body;
|
||||
sshLogger.info('Processing JSON data for SSH host update', { operation: 'host_update', hostId: parseInt(hostId), userId });
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -256,11 +259,11 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
||||
username,
|
||||
authType: effectiveAuthType,
|
||||
credentialId: credentialId || null,
|
||||
pin: !pin ? 1 : 0,
|
||||
enableTerminal: !enableTerminal ? 1 : 0,
|
||||
enableTunnel: !enableTunnel ? 1 : 0,
|
||||
pin: pin ? 1 : 0,
|
||||
enableTerminal: enableTerminal ? 1 : 0,
|
||||
enableTunnel: enableTunnel ? 1 : 0,
|
||||
tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) : null,
|
||||
enableFileManager: !enableFileManager ? 1 : 0,
|
||||
enableFileManager: enableFileManager ? 1 : 0,
|
||||
defaultPath: defaultPath || null,
|
||||
};
|
||||
|
||||
@@ -271,7 +274,6 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.keyType = null;
|
||||
sshLogger.info('SSH host update configured for password authentication', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port });
|
||||
} else if (effectiveAuthType === 'key') {
|
||||
if (key) {
|
||||
sshDataObj.key = key;
|
||||
@@ -283,16 +285,37 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
||||
sshDataObj.keyType = keyType;
|
||||
}
|
||||
sshDataObj.password = null;
|
||||
sshLogger.info('SSH host update configured for key authentication', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, keyType });
|
||||
}
|
||||
|
||||
try {
|
||||
sshLogger.info('Attempting to update SSH host in database', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, authType: effectiveAuthType });
|
||||
await db.update(sshData)
|
||||
.set(sshDataObj)
|
||||
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
||||
sshLogger.success('SSH host updated successfully', { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, authType: effectiveAuthType, enableTerminal, enableTunnel, enableFileManager });
|
||||
res.json({message: 'SSH data updated'});
|
||||
|
||||
const updatedHosts = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
||||
|
||||
if (updatedHosts.length === 0) {
|
||||
sshLogger.warn('Updated host not found after update', { operation: 'host_update', hostId: parseInt(hostId), userId });
|
||||
return res.status(404).json({error: 'Host not found after update'});
|
||||
}
|
||||
|
||||
const updatedHost = updatedHosts[0];
|
||||
const baseHost = {
|
||||
...updatedHost,
|
||||
tags: typeof updatedHost.tags === 'string' ? (updatedHost.tags ? updatedHost.tags.split(',').filter(Boolean) : []) : [],
|
||||
pin: !!updatedHost.pin,
|
||||
enableTerminal: !!updatedHost.enableTerminal,
|
||||
enableTunnel: !!updatedHost.enableTunnel,
|
||||
tunnelConnections: updatedHost.tunnelConnections ? JSON.parse(updatedHost.tunnelConnections) : [],
|
||||
enableFileManager: !!updatedHost.enableFileManager,
|
||||
};
|
||||
|
||||
const resolvedHost = await resolveHostCredentials(baseHost) || baseHost;
|
||||
|
||||
res.json(resolvedHost);
|
||||
} catch (err) {
|
||||
sshLogger.error('Failed to update SSH host in database', err, { operation: 'host_update', hostId: parseInt(hostId), userId, name, ip, port, authType: effectiveAuthType });
|
||||
res.status(500).json({error: 'Failed to update SSH data'});
|
||||
@@ -303,19 +326,16 @@ router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Re
|
||||
// GET /ssh/host
|
||||
router.get('/db/host', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
sshLogger.info('SSH hosts fetch request received', { operation: 'host_fetch', userId });
|
||||
if (!isNonEmptyString(userId)) {
|
||||
sshLogger.warn('Invalid userId for SSH data fetch', { operation: 'host_fetch', userId });
|
||||
return res.status(400).json({error: 'Invalid userId'});
|
||||
}
|
||||
try {
|
||||
sshLogger.info('Fetching SSH hosts from database', { operation: 'host_fetch', userId });
|
||||
const data = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(eq(sshData.userId, userId));
|
||||
|
||||
sshLogger.info('Processing SSH hosts and resolving credentials', { operation: 'host_fetch', userId, hostCount: data.length });
|
||||
const result = await Promise.all(data.map(async (row: any) => {
|
||||
const baseHost = {
|
||||
...row,
|
||||
@@ -330,7 +350,6 @@ router.get('/db/host', authenticateJWT, async (req: Request, res: Response) => {
|
||||
return await resolveHostCredentials(baseHost) || baseHost;
|
||||
}));
|
||||
|
||||
sshLogger.success('SSH hosts fetched successfully', { operation: 'host_fetch', userId, hostCount: result.length });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
sshLogger.error('Failed to fetch SSH hosts from database', err, { operation: 'host_fetch', userId });
|
||||
@@ -343,14 +362,12 @@ router.get('/db/host', authenticateJWT, async (req: Request, res: Response) => {
|
||||
router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const hostId = req.params.id;
|
||||
const userId = (req as any).userId;
|
||||
sshLogger.info('SSH host fetch by ID request received', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId });
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId) {
|
||||
sshLogger.warn('Invalid userId or hostId for SSH host fetch by ID', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId });
|
||||
return res.status(400).json({error: 'Invalid userId or hostId'});
|
||||
}
|
||||
try {
|
||||
sshLogger.info('Fetching SSH host by ID from database', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId });
|
||||
const data = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
@@ -372,7 +389,6 @@ router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response)
|
||||
enableFileManager: !!host.enableFileManager,
|
||||
};
|
||||
|
||||
sshLogger.success('SSH host fetched by ID successfully', { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId, hostName: result.name });
|
||||
res.json(await resolveHostCredentials(result) || result);
|
||||
} catch (err) {
|
||||
sshLogger.error('Failed to fetch SSH host by ID from database', err, { operation: 'host_fetch_by_id', hostId: parseInt(hostId), userId });
|
||||
@@ -385,17 +401,14 @@ router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response)
|
||||
router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const hostId = req.params.id;
|
||||
sshLogger.info('SSH host deletion request received', { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
||||
|
||||
if (!isNonEmptyString(userId) || !hostId) {
|
||||
sshLogger.warn('Invalid userId or hostId for SSH host delete', { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
||||
return res.status(400).json({error: 'Invalid userId or id'});
|
||||
}
|
||||
try {
|
||||
sshLogger.info('Attempting to delete SSH host from database', { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
||||
const result = await db.delete(sshData)
|
||||
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId)));
|
||||
sshLogger.success('SSH host deleted successfully', { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
||||
res.json({message: 'SSH host deleted'});
|
||||
} catch (err) {
|
||||
sshLogger.error('Failed to delete SSH host from database', err, { operation: 'host_delete', hostId: parseInt(hostId), userId });
|
||||
@@ -722,4 +735,66 @@ async function resolveHostCredentials(host: any): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
// Route: Rename folder (requires JWT)
|
||||
// PUT /ssh/db/folders/rename
|
||||
router.put('/folders/rename', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const { oldName, newName } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !oldName || !newName) {
|
||||
sshLogger.warn('Invalid data for folder rename');
|
||||
return res.status(400).json({error: 'Old name and new name are required'});
|
||||
}
|
||||
|
||||
if (oldName === newName) {
|
||||
return res.json({message: 'Folder name unchanged'});
|
||||
}
|
||||
|
||||
try {
|
||||
// Update all hosts with the old folder name
|
||||
const updatedHosts = await db
|
||||
.update(sshData)
|
||||
.set({
|
||||
folder: newName,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
.where(and(
|
||||
eq(sshData.userId, userId),
|
||||
eq(sshData.folder, oldName)
|
||||
))
|
||||
.returning();
|
||||
|
||||
// Update all credentials with the old folder name
|
||||
const updatedCredentials = await db
|
||||
.update(sshCredentials)
|
||||
.set({
|
||||
folder: newName,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
.where(and(
|
||||
eq(sshCredentials.userId, userId),
|
||||
eq(sshCredentials.folder, oldName)
|
||||
))
|
||||
.returning();
|
||||
|
||||
sshLogger.success('Folder renamed successfully', {
|
||||
operation: 'folder_rename',
|
||||
userId,
|
||||
oldName,
|
||||
newName,
|
||||
updatedHosts: updatedHosts.length,
|
||||
updatedCredentials: updatedCredentials.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Folder renamed successfully',
|
||||
updatedHosts: updatedHosts.length,
|
||||
updatedCredentials: updatedCredentials.length
|
||||
});
|
||||
} catch (err) {
|
||||
sshLogger.error('Failed to rename folder', err, { operation: 'folder_rename', userId, oldName, newName });
|
||||
res.status(500).json({error: 'Failed to rename folder'});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -119,7 +119,7 @@ function authenticateJWT(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
|
||||
(req as any).userId = payload.userId;
|
||||
authLogger.debug('JWT authentication successful', { operation: 'auth', userId: payload.userId, method: req.method, url: req.url });
|
||||
// JWT authentication successful
|
||||
next();
|
||||
} catch (err) {
|
||||
authLogger.warn('Invalid or expired token', { operation: 'auth', method: req.method, url: req.url, error: err });
|
||||
|
||||
@@ -49,7 +49,7 @@ function scheduleSessionCleanup(sessionId: string) {
|
||||
app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
||||
const {sessionId, hostId, ip, port, username, password, sshKey, keyPassword, authType, credentialId, userId} = req.body;
|
||||
|
||||
fileLogger.info('File manager SSH connection request received', { operation: 'file_connect', sessionId, hostId, ip, port, username, authType, hasCredentialId: !!credentialId });
|
||||
// Connection request received
|
||||
|
||||
if (!sessionId || !ip || !username || !port) {
|
||||
fileLogger.warn('Missing SSH connection parameters for file manager', { operation: 'file_connect', sessionId, hasIp: !!ip, hasUsername: !!username, hasPort: !!port });
|
||||
@@ -57,14 +57,12 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
||||
}
|
||||
|
||||
if (sshSessions[sessionId]?.isConnected) {
|
||||
fileLogger.info('Cleaning up existing SSH session', { operation: 'file_connect', sessionId });
|
||||
cleanupSession(sessionId);
|
||||
}
|
||||
const client = new SSHClient();
|
||||
|
||||
let resolvedCredentials = {password, sshKey, keyPassword, authType};
|
||||
if (credentialId && hostId && userId) {
|
||||
fileLogger.info('Resolving credentials from database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, userId });
|
||||
try {
|
||||
const credentials = await db
|
||||
.select()
|
||||
@@ -82,15 +80,14 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
||||
keyPassword: credential.keyPassword,
|
||||
authType: credential.authType
|
||||
};
|
||||
fileLogger.success('Credentials resolved successfully for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, authType: credential.authType });
|
||||
} else {
|
||||
fileLogger.warn('No credentials found in database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId });
|
||||
fileLogger.warn('No credentials found in database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, userId });
|
||||
}
|
||||
} catch (error) {
|
||||
fileLogger.warn('Failed to resolve credentials from database for file manager', { operation: 'file_connect', sessionId, hostId, credentialId, error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
}
|
||||
} else {
|
||||
fileLogger.info('Using direct credentials for file manager connection', { operation: 'file_connect', sessionId, hostId, authType });
|
||||
} else if (credentialId && hostId) {
|
||||
fileLogger.warn('Missing userId for credential resolution in file manager', { operation: 'file_connect', sessionId, hostId, credentialId, hasUserId: !!userId });
|
||||
}
|
||||
|
||||
const config: any = {
|
||||
@@ -137,7 +134,6 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
||||
};
|
||||
|
||||
if (resolvedCredentials.sshKey && resolvedCredentials.sshKey.trim()) {
|
||||
fileLogger.info('Configuring SSH key authentication for file manager', { operation: 'file_connect', sessionId, hostId, hasKeyPassword: !!resolvedCredentials.keyPassword });
|
||||
try {
|
||||
if (!resolvedCredentials.sshKey.includes('-----BEGIN') || !resolvedCredentials.sshKey.includes('-----END')) {
|
||||
throw new Error('Invalid private key format');
|
||||
@@ -149,13 +145,11 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
||||
|
||||
if (resolvedCredentials.keyPassword) config.passphrase = resolvedCredentials.keyPassword;
|
||||
|
||||
fileLogger.success('SSH key authentication configured successfully for file manager', { operation: 'file_connect', sessionId, hostId });
|
||||
} catch (keyError) {
|
||||
fileLogger.error('SSH key format error for file manager', { operation: 'file_connect', sessionId, hostId, error: keyError.message });
|
||||
return res.status(400).json({error: 'Invalid SSH key format'});
|
||||
}
|
||||
} else if (resolvedCredentials.password && resolvedCredentials.password.trim()) {
|
||||
fileLogger.info('Configuring password authentication for file manager', { operation: 'file_connect', sessionId, hostId });
|
||||
config.password = resolvedCredentials.password;
|
||||
} else {
|
||||
fileLogger.warn('No authentication method provided for file manager', { operation: 'file_connect', sessionId, hostId });
|
||||
@@ -167,7 +161,6 @@ app.post('/ssh/file_manager/ssh/connect', async (req, res) => {
|
||||
client.on('ready', () => {
|
||||
if (responseSent) return;
|
||||
responseSent = true;
|
||||
fileLogger.success('SSH connection established for file manager', { operation: 'file_connect', sessionId, hostId, ip, port, username, authType: resolvedCredentials.authType });
|
||||
sshSessions[sessionId] = {client, isConnected: true, lastActive: Date.now()};
|
||||
res.json({status: 'success', message: 'SSH connection established'});
|
||||
});
|
||||
@@ -377,7 +370,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
writeStream.on('finish', () => {
|
||||
if (hasError || hasFinished) return;
|
||||
hasFinished = true;
|
||||
fileLogger.success(`File written successfully via SFTP: ${filePath}`);
|
||||
if (!res.headersSent) {
|
||||
res.json({message: 'File written successfully', path: filePath});
|
||||
}
|
||||
@@ -386,7 +378,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
writeStream.on('close', () => {
|
||||
if (hasError || hasFinished) return;
|
||||
hasFinished = true;
|
||||
fileLogger.success(`File written successfully via SFTP: ${filePath}`);
|
||||
if (!res.headersSent) {
|
||||
res.json({message: 'File written successfully', path: filePath});
|
||||
}
|
||||
@@ -440,7 +431,6 @@ app.post('/ssh/file_manager/ssh/writeFile', (req, res) => {
|
||||
|
||||
|
||||
if (outputData.includes('SUCCESS')) {
|
||||
fileLogger.success(`File written successfully via fallback: ${filePath}`);
|
||||
if (!res.headersSent) {
|
||||
res.json({message: 'File written successfully', path: filePath});
|
||||
}
|
||||
@@ -536,8 +526,6 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
writeStream.on('finish', () => {
|
||||
if (hasError || hasFinished) return;
|
||||
hasFinished = true;
|
||||
|
||||
fileLogger.success(`File uploaded successfully via SFTP: ${fullPath}`);
|
||||
if (!res.headersSent) {
|
||||
res.json({message: 'File uploaded successfully', path: fullPath});
|
||||
}
|
||||
@@ -546,8 +534,6 @@ app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => {
|
||||
writeStream.on('close', () => {
|
||||
if (hasError || hasFinished) return;
|
||||
hasFinished = true;
|
||||
|
||||
fileLogger.success(`File uploaded successfully via SFTP: ${fullPath}`);
|
||||
if (!res.headersSent) {
|
||||
res.json({message: 'File uploaded successfully', path: fullPath});
|
||||
}
|
||||
|
||||
@@ -101,8 +101,6 @@ async function fetchHostById(id: number): Promise<SSHHostWithCredentials | undef
|
||||
|
||||
async function resolveHostCredentials(host: any): Promise<SSHHostWithCredentials | undefined> {
|
||||
try {
|
||||
statsLogger.info('Resolving credentials for host', { operation: 'host_credential_resolve', hostId: host.id, hostName: host.name, hasCredentialId: !!host.credentialId });
|
||||
|
||||
const baseHost: any = {
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
@@ -124,7 +122,6 @@ async function resolveHostCredentials(host: any): Promise<SSHHostWithCredentials
|
||||
};
|
||||
|
||||
if (host.credentialId) {
|
||||
statsLogger.info('Fetching credentials from database', { operation: 'host_credential_resolve', hostId: host.id, credentialId: host.credentialId, userId: host.userId });
|
||||
try {
|
||||
const credentials = await db
|
||||
.select()
|
||||
@@ -152,6 +149,7 @@ async function resolveHostCredentials(host: any): Promise<SSHHostWithCredentials
|
||||
if (credential.keyType) {
|
||||
baseHost.keyType = credential.keyType;
|
||||
}
|
||||
|
||||
} else {
|
||||
statsLogger.warn(`Credential ${host.credentialId} not found for host ${host.id}, using legacy data`);
|
||||
addLegacyCredentials(baseHost, host);
|
||||
@@ -437,23 +435,19 @@ function tcpPing(host: string, port: number, timeoutMs = 5000): Promise<boolean>
|
||||
}
|
||||
|
||||
async function pollStatusesOnce(): Promise<void> {
|
||||
statsLogger.info('Starting status polling for all hosts', { operation: 'status_poll' });
|
||||
const hosts = await fetchAllHosts();
|
||||
if (hosts.length === 0) {
|
||||
statsLogger.warn('No hosts retrieved for status polling', { operation: 'status_poll' });
|
||||
return;
|
||||
}
|
||||
|
||||
statsLogger.info('Polling status for hosts', { operation: 'status_poll', hostCount: hosts.length, hostIds: hosts.map(h => h.id) });
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const checks = hosts.map(async (h) => {
|
||||
statsLogger.info('Checking host status', { operation: 'status_poll', hostId: h.id, hostName: h.name, ip: h.ip, port: h.port });
|
||||
const isOnline = await tcpPing(h.ip, h.port, 5000);
|
||||
const now = new Date().toISOString();
|
||||
const statusEntry: StatusEntry = {status: isOnline ? 'online' : 'offline', lastChecked: now};
|
||||
hostStatuses.set(h.id, statusEntry);
|
||||
statsLogger.info('Host status check completed', { operation: 'status_poll', hostId: h.id, hostName: h.name, status: isOnline ? 'online' : 'offline' });
|
||||
return isOnline;
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
|
||||
|
||||
ws.on('close', () => {
|
||||
sshLogger.info('WebSocket connection closed', { operation: 'websocket_disconnect' });
|
||||
cleanupSSH();
|
||||
});
|
||||
|
||||
@@ -53,7 +52,6 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
sshLogger.info('SSH disconnect requested', { operation: 'ssh_disconnect' });
|
||||
cleanupSSH();
|
||||
break;
|
||||
|
||||
@@ -127,14 +125,14 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
}, 60000);
|
||||
|
||||
let resolvedCredentials = {password, key, keyPassword, keyType, authType};
|
||||
if (credentialId && id) {
|
||||
if (credentialId && id && hostConfig.userId) {
|
||||
try {
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(and(
|
||||
eq(sshCredentials.id, credentialId),
|
||||
eq(sshCredentials.userId, hostConfig.userId || '')
|
||||
eq(sshCredentials.userId, hostConfig.userId)
|
||||
));
|
||||
|
||||
if (credentials.length > 0) {
|
||||
@@ -146,15 +144,18 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
keyType: credential.keyType,
|
||||
authType: credential.authType
|
||||
};
|
||||
} else {
|
||||
sshLogger.warn(`No credentials found for host ${id}`, { operation: 'ssh_credentials', hostId: id, credentialId, userId: hostConfig.userId });
|
||||
}
|
||||
} catch (error) {
|
||||
sshLogger.warn(`Failed to resolve credentials for host ${id}`, { operation: 'ssh_credentials', hostId: id, credentialId, error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
}
|
||||
} else if (credentialId && id) {
|
||||
sshLogger.warn('Missing userId for credential resolution in terminal', { operation: 'ssh_credentials', hostId: id, credentialId, hasUserId: !!hostConfig.userId });
|
||||
}
|
||||
|
||||
sshConn.on('ready', () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
sshLogger.success('SSH connection established', { operation: 'ssh_connect', hostId: id, ip, port, username, authType: resolvedCredentials.authType });
|
||||
|
||||
|
||||
sshConn!.shell({
|
||||
@@ -175,7 +176,6 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
});
|
||||
|
||||
stream.on('close', () => {
|
||||
sshLogger.info('SSH stream closed', { operation: 'ssh_stream', hostId: id, ip, port, username });
|
||||
ws.send(JSON.stringify({type: 'disconnected', message: 'Connection lost'}));
|
||||
});
|
||||
|
||||
@@ -219,7 +219,6 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
|
||||
sshConn.on('close', () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
|
||||
cleanupSSH(connectionTimeout);
|
||||
});
|
||||
|
||||
|
||||
@@ -382,7 +382,9 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
||||
const tunnelName = tunnelConfig.name;
|
||||
const tunnelMarker = getTunnelMarker(tunnelName);
|
||||
|
||||
tunnelLogger.info('SSH tunnel connection attempt started', { operation: 'tunnel_connect', tunnelName, retryAttempt, sourceIP: tunnelConfig.sourceIP, sourcePort: tunnelConfig.sourceSSHPort });
|
||||
if (retryAttempt === 0) {
|
||||
tunnelLogger.info('SSH tunnel connection attempt started', { operation: 'tunnel_connect', tunnelName, sourceIP: tunnelConfig.sourceIP, sourcePort: tunnelConfig.sourceSSHPort });
|
||||
}
|
||||
|
||||
if (manualDisconnects.has(tunnelName)) {
|
||||
tunnelLogger.info('Tunnel connection cancelled due to manual disconnect', { operation: 'tunnel_connect', tunnelName });
|
||||
@@ -394,14 +396,10 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
||||
if (retryAttempt === 0) {
|
||||
retryExhaustedTunnels.delete(tunnelName);
|
||||
retryCounters.delete(tunnelName);
|
||||
tunnelLogger.info('Reset retry state for tunnel', { operation: 'tunnel_connect', tunnelName });
|
||||
} else {
|
||||
tunnelLogger.warn('Tunnel connection retry attempt', { operation: 'tunnel_connect', tunnelName, retryAttempt });
|
||||
}
|
||||
|
||||
const currentStatus = connectionStatus.get(tunnelName);
|
||||
if (!currentStatus || currentStatus.status !== CONNECTION_STATES.WAITING) {
|
||||
tunnelLogger.info('Broadcasting tunnel connecting status', { operation: 'tunnel_connect', tunnelName, retryAttempt });
|
||||
broadcastTunnelStatus(tunnelName, {
|
||||
connected: false,
|
||||
status: CONNECTION_STATES.CONNECTING,
|
||||
@@ -428,7 +426,6 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
||||
};
|
||||
|
||||
if (tunnelConfig.sourceCredentialId && tunnelConfig.sourceUserId) {
|
||||
tunnelLogger.info('Resolving source credentials from database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId, userId: tunnelConfig.sourceUserId });
|
||||
try {
|
||||
const credentials = await db
|
||||
.select()
|
||||
@@ -447,15 +444,12 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
||||
keyType: credential.keyType,
|
||||
authMethod: credential.authType
|
||||
};
|
||||
tunnelLogger.success('Source credentials resolved successfully', { operation: 'tunnel_connect', tunnelName, credentialId: credential.id, authType: credential.authType });
|
||||
} else {
|
||||
tunnelLogger.warn('No source credentials found in database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId });
|
||||
}
|
||||
} catch (error) {
|
||||
tunnelLogger.warn('Failed to resolve source credentials from database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.sourceCredentialId, error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
}
|
||||
} else {
|
||||
tunnelLogger.info('Using direct source credentials from tunnel config', { operation: 'tunnel_connect', tunnelName, authMethod: tunnelConfig.sourceAuthMethod });
|
||||
}
|
||||
|
||||
// Resolve endpoint credentials if tunnel config has endpointCredentialId
|
||||
@@ -486,10 +480,14 @@ async function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): P
|
||||
keyType: credential.keyType,
|
||||
authMethod: credential.authType
|
||||
};
|
||||
} else {
|
||||
tunnelLogger.warn('No endpoint credentials found in database', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.endpointCredentialId });
|
||||
}
|
||||
} catch (error) {
|
||||
tunnelLogger.warn(`Failed to resolve endpoint credentials for tunnel ${tunnelName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
} else if (tunnelConfig.endpointCredentialId) {
|
||||
tunnelLogger.warn('Missing userId for endpoint credential resolution', { operation: 'tunnel_connect', tunnelName, credentialId: tunnelConfig.endpointCredentialId, hasUserId: !!tunnelConfig.endpointUserId });
|
||||
}
|
||||
|
||||
const conn = new Client();
|
||||
|
||||
@@ -12,12 +12,6 @@ import { systemLogger } from './utils/logger.js';
|
||||
try {
|
||||
systemLogger.info("Initializing backend services...", { operation: 'startup' });
|
||||
|
||||
systemLogger.info("Loading database service...", { operation: 'database_init' });
|
||||
systemLogger.info("Loading SSH terminal service...", { operation: 'terminal_init' });
|
||||
systemLogger.info("Loading SSH tunnel service...", { operation: 'tunnel_init' });
|
||||
systemLogger.info("Loading file manager service...", { operation: 'file_manager_init' });
|
||||
systemLogger.info("Loading server stats service...", { operation: 'stats_init' });
|
||||
|
||||
systemLogger.success("All backend services initialized successfully", {
|
||||
operation: 'startup_complete',
|
||||
services: ['database', 'terminal', 'tunnel', 'file_manager', 'stats']
|
||||
@@ -25,13 +19,11 @@ import { systemLogger } from './utils/logger.js';
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
systemLogger.info("Received SIGINT signal, initiating graceful shutdown...", { operation: 'shutdown' });
|
||||
systemLogger.info("Shutting down all services...", { operation: 'shutdown' });
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
systemLogger.info("Received SIGTERM signal, initiating graceful shutdown...", { operation: 'shutdown' });
|
||||
systemLogger.info("Shutting down all services...", { operation: 'shutdown' });
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user