Service stack (backend): - webserver.ts rewritten: apache2/php-fpm routes replacing nginx; per-account Apache log endpoint; PHP-FPM reload per version; apache2ctl configtest/reload - accounts.ts: added PUT /:id/shell to toggle /bin/bash vs /usr/sbin/nologin, runs usermod -s and writes audit log New UI features (nixserver): - ShellAccess page: grant/revoke SSH access per account via AccountPicker - ApacheLogs page: live log viewer (access/error) per account; replaces stub - NginxLogs: re-exports ApacheLogs (NixPanel uses Apache, not NGINX) - WebServer page: shows apache2, php8.2-fpm, php8.3-fpm status with reload buttons; config test; Apache log viewer Bug fixes: - Accounts.tsx: Lucide icon size prop type widened (string|number); removed unused Users import; dropped duplicate NGINX log card - Removed soon badge from ShellAccess and ApacheLogs entries Project: - CHANGELOG.md created with full history from 0.2.x through 0.4.0 - All packages bumped from 0.3.2 to 0.4.0
100 lines
2.6 KiB
TypeScript
100 lines
2.6 KiB
TypeScript
import { config } from '../config.js'
|
|
|
|
let licenseValid = false
|
|
let lastChecked: Date | null = null
|
|
|
|
/**
|
|
* Validate the license with the licensing server on startup.
|
|
* Throws if the license is invalid or the server is unreachable.
|
|
*/
|
|
export async function validateOnStartup(): Promise<void> {
|
|
if (!config.licensing.licenseKey) {
|
|
if (config.env === 'development') {
|
|
licenseValid = true
|
|
return
|
|
}
|
|
throw new Error('LICENSE_KEY is not set. Set it in .env to activate NixPanel.')
|
|
}
|
|
|
|
await checkLicense()
|
|
}
|
|
|
|
/**
|
|
* Send a heartbeat to the licensing server every 6 hours.
|
|
* If the license becomes invalid, logs a critical event but does NOT shut down —
|
|
* the panel keeps running to avoid accidental outages.
|
|
*/
|
|
export async function heartbeat(): Promise<void> {
|
|
if (!config.licensing.licenseKey) return
|
|
|
|
try {
|
|
await checkLicense()
|
|
} catch (err) {
|
|
console.error('[license] Heartbeat failed:', err)
|
|
// Log to DB if available — non-fatal, don't crash
|
|
try {
|
|
const { default: prisma } = await import('../db/client.js')
|
|
await prisma.securityEvent.create({
|
|
data: {
|
|
type: 'license_heartbeat_failed',
|
|
severity: 'warning',
|
|
message: `License heartbeat failed: ${String(err)}`,
|
|
},
|
|
})
|
|
} catch {
|
|
// DB may be unavailable — ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
export function isLicenseValid(): boolean {
|
|
return licenseValid
|
|
}
|
|
|
|
export function getLastChecked(): Date | null {
|
|
return lastChecked
|
|
}
|
|
|
|
async function checkLicense(): Promise<void> {
|
|
const url = `${config.licensing.serverUrl}/api/v1/validate`
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
licenseKey: config.licensing.licenseKey,
|
|
serverIp: await getServerIp(),
|
|
}),
|
|
signal: AbortSignal.timeout(15_000),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
licenseValid = false
|
|
throw new Error(`License server returned ${response.status}`)
|
|
}
|
|
|
|
const data = await response.json() as { valid: boolean; message?: string }
|
|
licenseValid = data.valid
|
|
lastChecked = new Date()
|
|
|
|
if (!data.valid) {
|
|
throw new Error(data.message ?? 'License is not valid')
|
|
}
|
|
}
|
|
|
|
async function getServerIp(): Promise<string> {
|
|
try {
|
|
const { createConnection } = await import('net')
|
|
return new Promise((resolve) => {
|
|
const socket = createConnection(80, '8.8.8.8')
|
|
socket.on('connect', () => {
|
|
resolve(socket.localAddress ?? '')
|
|
socket.destroy()
|
|
})
|
|
socket.on('error', () => resolve(''))
|
|
})
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|