added server.js for frontend when not using nginx in deployment

This commit is contained in:
Muhammad Ibrahim
2025-09-18 22:59:50 +01:00
parent 2d7a3c3103
commit 51d6dd63b1
10 changed files with 355 additions and 200 deletions

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "settings" ADD COLUMN "last_update_check" TIMESTAMP(3),
ADD COLUMN "latest_version" TEXT,
ADD COLUMN "update_available" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -187,6 +187,9 @@ model Settings {
githubRepoUrl String @map("github_repo_url") @default("git@github.com:9technologygroup/patchmon.net.git") // GitHub repository URL for version checking
repositoryType String @map("repository_type") @default("public") // "public" or "private"
sshKeyPath String? @map("ssh_key_path") // Optional SSH key path for deploy key authentication
lastUpdateCheck DateTime? @map("last_update_check") // When the system last checked for updates
updateAvailable Boolean @map("update_available") @default(false) // Whether an update is available
latestVersion String? @map("latest_version") // Latest available version
createdAt DateTime @map("created_at") @default(now())
updatedAt DateTime @map("updated_at") @updatedAt

View File

@@ -142,202 +142,36 @@ router.post('/test-ssh-key', authenticateToken, requireManageSettings, async (re
// Check for updates from GitHub
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
try {
// Get GitHub repo URL from settings
// Get cached update information from settings
const settings = await prisma.settings.findFirst();
console.log('Settings retrieved for version check:', {
id: settings?.id,
githubRepoUrl: settings?.githubRepoUrl,
repositoryType: settings?.repositoryType
if (!settings) {
return res.status(400).json({ error: 'Settings not found' });
}
const currentVersion = '1.2.4';
const latestVersion = settings.latestVersion || currentVersion;
const isUpdateAvailable = settings.updateAvailable || false;
const lastUpdateCheck = settings.lastUpdateCheck;
res.json({
currentVersion,
latestVersion,
isUpdateAvailable,
lastUpdateCheck,
repositoryType: settings.repositoryType || 'public',
latestRelease: {
tagName: latestVersion ? `v${latestVersion}` : null,
version: latestVersion,
repository: settings.githubRepoUrl ? settings.githubRepoUrl.split('/').slice(-2).join('/') : null,
accessMethod: settings.repositoryType === 'private' ? 'ssh' : 'api'
}
});
if (!settings || !settings.githubRepoUrl) {
return res.status(400).json({ error: 'GitHub repository URL not configured' });
}
// Extract owner and repo from GitHub URL
// Support both SSH and HTTPS formats:
// git@github.com:owner/repo.git
// https://github.com/owner/repo.git
const repoUrl = settings.githubRepoUrl;
console.log('Using repository URL:', repoUrl);
let owner, repo;
if (repoUrl.includes('git@github.com:')) {
const match = repoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/);
if (match) {
[, owner, repo] = match;
}
} else if (repoUrl.includes('github.com/')) {
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (match) {
[, owner, repo] = match;
}
}
console.log('Extracted owner and repo:', { owner, repo });
if (!owner || !repo) {
return res.status(400).json({ error: 'Invalid GitHub repository URL format' });
}
// Determine repository type and set up appropriate access method
const repositoryType = settings.repositoryType || 'public';
const isPrivate = repositoryType === 'private';
let latestTag;
if (isPrivate) {
// Use SSH with deploy keys for private repositories
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
try {
let sshKeyPath = null;
// First, try to use the configured SSH key path from settings
if (settings.sshKeyPath) {
try {
require('fs').accessSync(settings.sshKeyPath);
sshKeyPath = settings.sshKeyPath;
console.log(`Using configured SSH key at: ${sshKeyPath}`);
} catch (e) {
console.warn(`Configured SSH key path not accessible: ${settings.sshKeyPath}`);
}
}
// If no configured path or it's not accessible, try common locations
if (!sshKeyPath) {
const possibleKeyPaths = [
'/root/.ssh/id_ed25519', // Root user (if service runs as root)
'/root/.ssh/id_rsa', // Root user RSA key
'/home/patchmon/.ssh/id_ed25519', // PatchMon user
'/home/patchmon/.ssh/id_rsa', // PatchMon user RSA key
'/var/www/.ssh/id_ed25519', // Web user
'/var/www/.ssh/id_rsa' // Web user RSA key
];
for (const path of possibleKeyPaths) {
try {
require('fs').accessSync(path);
sshKeyPath = path;
console.log(`Found SSH key at: ${path}`);
break;
} catch (e) {
// Key not found at this path, try next
}
}
}
if (!sshKeyPath) {
throw new Error('No SSH deploy key found. Please configure the SSH key path in settings or ensure a deploy key is installed in one of the expected locations.');
}
const env = {
...process.env,
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes`
};
// Fetch the latest tag using SSH with deploy key
const { stdout: sshLatestTag } = await execAsync(
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
{
timeout: 10000,
env: env
}
);
latestTag = sshLatestTag;
} catch (sshError) {
console.error('SSH Git error:', sshError.message);
throw sshError;
}
} else {
// Use GitHub API for public repositories (no authentication required)
try {
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
const response = await fetch(httpsRepoUrl, {
method: 'GET',
headers: {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'PatchMon-Server/1.2.4'
},
timeout: 10000
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
const releaseData = await response.json();
latestTag = releaseData.tag_name;
} catch (apiError) {
console.error('GitHub API error:', apiError.message);
throw apiError;
}
}
const latestVersion = latestTag.trim().replace('v', ''); // Remove 'v' prefix
const currentVersion = '1.2.4';
// Simple version comparison (assumes semantic versioning)
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
res.json({
currentVersion,
latestVersion,
isUpdateAvailable,
repositoryType,
latestRelease: {
tagName: latestTag.trim(),
version: latestVersion,
repository: `${owner}/${repo}`,
accessMethod: isPrivate ? 'ssh' : 'api',
...(isPrivate && { sshUrl: `git@github.com:${owner}/${repo}.git`, sshKeyUsed: settings.sshKeyPath })
}
});
} catch (error) {
console.error('Version check error:', error.message);
if (error.message.includes('Permission denied') || error.message.includes('Host key verification failed')) {
return res.status(403).json({
error: 'Access denied to repository',
suggestion: isPrivate
? 'Ensure your deploy key is properly configured and has access to the repository. Check that the key has read access to the repository.'
: 'Check that the repository URL is correct and the repository is public.'
});
}
if (error.message.includes('not found') || error.message.includes('does not exist')) {
return res.status(404).json({
error: 'Repository not found',
suggestion: 'Check that the repository URL is correct and accessible.'
});
}
if (error.message.includes('No SSH deploy key found')) {
return res.status(400).json({
error: 'No SSH deploy key found',
suggestion: 'Please install a deploy key in one of the expected locations: /root/.ssh/, /home/patchmon/.ssh/, or /var/www/.ssh/'
});
}
if (error.message.includes('GitHub API error')) {
return res.status(500).json({
error: 'Failed to fetch repository information via GitHub API',
details: error.message,
suggestion: 'Check that the repository URL is correct and the repository is public.'
});
}
return res.status(500).json({
error: 'Failed to fetch repository information',
details: error.message,
suggestion: isPrivate
? 'Check deploy key configuration and repository access permissions.'
: 'Check repository URL and ensure it is accessible.'
});
}
} catch (error) {
console.error('Error getting update information:', error);
res.status(500).json({ error: 'Failed to get update information' });
}
});
// Simple version comparison function

View File

@@ -18,6 +18,7 @@ const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes'
const repositoryRoutes = require('./routes/repositoryRoutes');
const versionRoutes = require('./routes/versionRoutes');
const tfaRoutes = require('./routes/tfaRoutes');
const updateScheduler = require('./services/updateScheduler');
// Initialize Prisma client
const prisma = new PrismaClient();
@@ -160,6 +161,7 @@ process.on('SIGTERM', async () => {
if (process.env.ENABLE_LOGGING === 'true') {
logger.info('SIGTERM received, shutting down gracefully');
}
updateScheduler.stop();
await prisma.$disconnect();
process.exit(0);
});
@@ -168,6 +170,7 @@ process.on('SIGINT', async () => {
if (process.env.ENABLE_LOGGING === 'true') {
logger.info('SIGINT received, shutting down gracefully');
}
updateScheduler.stop();
await prisma.$disconnect();
process.exit(0);
});
@@ -178,6 +181,9 @@ app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
logger.info(`Environment: ${process.env.NODE_ENV}`);
}
// Start update scheduler
updateScheduler.start();
});
module.exports = app;

View File

@@ -0,0 +1,247 @@
const { PrismaClient } = require('@prisma/client');
const { exec } = require('child_process');
const { promisify } = require('util');
const prisma = new PrismaClient();
const execAsync = promisify(exec);
class UpdateScheduler {
constructor() {
this.isRunning = false;
this.intervalId = null;
this.checkInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
}
// Start the scheduler
start() {
if (this.isRunning) {
console.log('Update scheduler is already running');
return;
}
console.log('🔄 Starting update scheduler...');
this.isRunning = true;
// Run initial check
this.checkForUpdates();
// Schedule regular checks
this.intervalId = setInterval(() => {
this.checkForUpdates();
}, this.checkInterval);
console.log(`✅ Update scheduler started - checking every ${this.checkInterval / (60 * 60 * 1000)} hours`);
}
// Stop the scheduler
stop() {
if (!this.isRunning) {
console.log('Update scheduler is not running');
return;
}
console.log('🛑 Stopping update scheduler...');
this.isRunning = false;
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
console.log('✅ Update scheduler stopped');
}
// Check for updates
async checkForUpdates() {
try {
console.log('🔍 Checking for updates...');
// Get settings
const settings = await prisma.settings.findFirst();
if (!settings || !settings.githubRepoUrl) {
console.log('⚠️ No GitHub repository configured, skipping update check');
return;
}
// Extract owner and repo from GitHub URL
const repoUrl = settings.githubRepoUrl;
let owner, repo;
if (repoUrl.includes('git@github.com:')) {
const match = repoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/);
if (match) {
[, owner, repo] = match;
}
} else if (repoUrl.includes('github.com/')) {
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
if (match) {
[, owner, repo] = match;
}
}
if (!owner || !repo) {
console.log('⚠️ Could not parse GitHub repository URL, skipping update check');
return;
}
let latestVersion;
const isPrivate = settings.repositoryType === 'private';
if (isPrivate) {
// Use SSH for private repositories
latestVersion = await this.checkPrivateRepo(settings, owner, repo);
} else {
// Use GitHub API for public repositories
latestVersion = await this.checkPublicRepo(owner, repo);
}
if (!latestVersion) {
console.log('⚠️ Could not determine latest version, skipping update check');
return;
}
const currentVersion = '1.2.4';
const isUpdateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
// Update settings with check results
await prisma.settings.update({
where: { id: settings.id },
data: {
lastUpdateCheck: new Date(),
updateAvailable: isUpdateAvailable,
latestVersion: latestVersion
}
});
console.log(`✅ Update check completed - Current: ${currentVersion}, Latest: ${latestVersion}, Update Available: ${isUpdateAvailable}`);
} catch (error) {
console.error('❌ Error checking for updates:', error.message);
// Update last check time even on error
try {
const settings = await prisma.settings.findFirst();
if (settings) {
await prisma.settings.update({
where: { id: settings.id },
data: {
lastUpdateCheck: new Date(),
updateAvailable: false
}
});
}
} catch (updateError) {
console.error('❌ Error updating last check time:', updateError.message);
}
}
}
// Check private repository using SSH
async checkPrivateRepo(settings, owner, repo) {
try {
let sshKeyPath = settings.sshKeyPath;
// Try to find SSH key if not configured
if (!sshKeyPath) {
const possibleKeyPaths = [
'/root/.ssh/id_ed25519',
'/root/.ssh/id_rsa',
'/home/patchmon/.ssh/id_ed25519',
'/home/patchmon/.ssh/id_rsa',
'/var/www/.ssh/id_ed25519',
'/var/www/.ssh/id_rsa'
];
for (const path of possibleKeyPaths) {
try {
require('fs').accessSync(path);
sshKeyPath = path;
break;
} catch (e) {
// Key not found at this path, try next
}
}
}
if (!sshKeyPath) {
throw new Error('No SSH deploy key found');
}
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
const env = {
...process.env,
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes`
};
const { stdout: sshLatestTag } = await execAsync(
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
{
timeout: 10000,
env: env
}
);
return sshLatestTag.trim().replace('v', '');
} catch (error) {
console.error('SSH Git error:', error.message);
throw error;
}
}
// Check public repository using GitHub API
async checkPublicRepo(owner, repo) {
try {
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
const response = await fetch(httpsRepoUrl, {
method: 'GET',
headers: {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'PatchMon-Server/1.2.4'
}
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
const releaseData = await response.json();
return releaseData.tag_name.replace('v', '');
} catch (error) {
console.error('GitHub API error:', error.message);
throw error;
}
}
// Compare version strings (semantic versioning)
compareVersions(version1, version2) {
const v1parts = version1.split('.').map(Number);
const v2parts = version2.split('.').map(Number);
const maxLength = Math.max(v1parts.length, v2parts.length);
for (let i = 0; i < maxLength; i++) {
const v1part = v1parts[i] || 0;
const v2part = v2parts[i] || 0;
if (v1part > v2part) return 1;
if (v1part < v2part) return -1;
}
return 0;
}
// Get scheduler status
getStatus() {
return {
isRunning: this.isRunning,
checkInterval: this.checkInterval,
nextCheck: this.isRunning ? new Date(Date.now() + this.checkInterval) : null
};
}
}
// Create singleton instance
const updateScheduler = new UpdateScheduler();
module.exports = updateScheduler;