Files
nixpanel-server/server/src/core/license.ts
T
NixReaper 8ff00c544c feat: v0.4.0 — Apache service stack, shell access, log viewer, changelog
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
2026-04-11 05:17:49 -05:00

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 ''
}
}