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
This commit is contained in:
@@ -1,34 +1,42 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { z } from 'zod'
|
||||
import { requireAdmin } from '../../middleware/auth.js'
|
||||
import { exec } from '../../core/exec.js'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { readdir } from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
export default async function webserverRoutes(fastify: FastifyInstance) {
|
||||
// GET /api/nixserver/webserver/status
|
||||
fastify.get('/status', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const [nginx, apache] = await Promise.all([
|
||||
exec('systemctl', ['is-active', 'nginx']),
|
||||
const [apache, fpm82, fpm83] = await Promise.all([
|
||||
exec('systemctl', ['is-active', 'apache2']),
|
||||
exec('systemctl', ['is-active', 'php8.2-fpm']),
|
||||
exec('systemctl', ['is-active', 'php8.3-fpm']),
|
||||
])
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
nginx: { active: nginx.stdout.trim() === 'active' },
|
||||
apache: { active: apache.stdout.trim() === 'active' },
|
||||
php82fpm: { active: fpm82.stdout.trim() === 'active' },
|
||||
php83fpm: { active: fpm83.stdout.trim() === 'active' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/nixserver/webserver/vhosts
|
||||
fastify.get('/vhosts', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const result = await exec('find', ['/etc/nginx/sites-enabled', '-name', '*.conf', '-type', 'f'])
|
||||
const files = result.stdout.split('\n').filter(Boolean)
|
||||
const vhosts = files.map(f => ({
|
||||
path: f,
|
||||
domain: f.split('/').pop()?.replace('.conf', '') ?? '',
|
||||
}))
|
||||
return reply.send({ success: true, data: vhosts })
|
||||
try {
|
||||
const files = await readdir('/etc/apache2/sites-enabled')
|
||||
const vhosts = files
|
||||
.filter(f => f.endsWith('.conf'))
|
||||
.map(f => ({
|
||||
path: `/etc/apache2/sites-enabled/${f}`,
|
||||
domain: f.replace('.conf', ''),
|
||||
}))
|
||||
return reply.send({ success: true, data: vhosts })
|
||||
} catch {
|
||||
return reply.send({ success: true, data: [] })
|
||||
}
|
||||
})
|
||||
|
||||
// GET /api/nixserver/webserver/vhosts/:domain
|
||||
@@ -38,36 +46,60 @@ export default async function webserverRoutes(fastify: FastifyInstance) {
|
||||
return reply.code(400).send({ success: false, error: 'Invalid domain' })
|
||||
}
|
||||
try {
|
||||
const content = await readFile(`/etc/nginx/sites-available/${domain}.conf`, 'utf8')
|
||||
const content = await readFile(`/etc/apache2/sites-available/${domain}.conf`, 'utf8')
|
||||
return reply.send({ success: true, data: { domain, config: content } })
|
||||
} catch {
|
||||
return reply.code(404).send({ success: false, error: 'Vhost not found' })
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/nixserver/webserver/nginx/test
|
||||
fastify.post('/nginx/test', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const result = await exec('nginx', ['-t'])
|
||||
// POST /api/nixserver/webserver/apache/test
|
||||
fastify.post('/apache/test', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const result = await exec('apache2ctl', ['configtest'])
|
||||
return reply.send({
|
||||
success: result.exitCode === 0,
|
||||
data: { output: result.stdout + result.stderr },
|
||||
})
|
||||
})
|
||||
|
||||
// POST /api/nixserver/webserver/nginx/reload
|
||||
fastify.post('/nginx/reload', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const test = await exec('nginx', ['-t'])
|
||||
// POST /api/nixserver/webserver/apache/reload
|
||||
fastify.post('/apache/reload', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const test = await exec('apache2ctl', ['configtest'])
|
||||
if (test.exitCode !== 0) {
|
||||
return reply.code(500).send({ success: false, error: `Config test failed: ${test.stderr}` })
|
||||
}
|
||||
const result = await exec('systemctl', ['reload', 'nginx'])
|
||||
const result = await exec('systemctl', ['reload', 'apache2'])
|
||||
return reply.send({ success: result.exitCode === 0, data: { output: result.stdout } })
|
||||
})
|
||||
|
||||
// POST /api/nixserver/webserver/fpm/reload
|
||||
fastify.post('/fpm/reload', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const { version } = (request.body ?? {}) as { version?: string }
|
||||
const service = version === '8.2' ? 'php8.2-fpm' : 'php8.3-fpm'
|
||||
const result = await exec('systemctl', ['reload', service])
|
||||
return reply.send({ success: result.exitCode === 0, data: { output: result.stdout } })
|
||||
})
|
||||
|
||||
// GET /api/nixserver/webserver/logs/:type — access or error log (last 200 lines)
|
||||
fastify.get('/logs/:type', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const { type } = request.params as { type: string }
|
||||
const logFile = type === 'error' ? '/var/log/nginx/error.log' : '/var/log/nginx/access.log'
|
||||
const logFile = type === 'error'
|
||||
? '/var/log/apache2/error.log'
|
||||
: '/var/log/apache2/access.log'
|
||||
const result = await exec('tail', ['-n', '200', logFile])
|
||||
return reply.send({ success: true, data: { lines: result.stdout.split('\n').filter(Boolean) } })
|
||||
})
|
||||
|
||||
// GET /api/nixserver/webserver/accounts/:username/logs/:type — per-account Apache logs
|
||||
fastify.get('/accounts/:username/logs/:type', { preHandler: [requireAdmin] }, async (request, reply) => {
|
||||
const { username, type } = request.params as { username: string; type: string }
|
||||
// Validate username — only alphanum, dash, underscore allowed
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
|
||||
return reply.code(400).send({ success: false, error: 'Invalid username' })
|
||||
}
|
||||
const logFile = type === 'error'
|
||||
? path.join('/home', username, 'logs', 'error.log')
|
||||
: path.join('/home', username, 'logs', 'access.log')
|
||||
const result = await exec('tail', ['-n', '200', logFile])
|
||||
return reply.send({ success: true, data: { lines: result.stdout.split('\n').filter(Boolean) } })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user