7 Commits
Author SHA1 Message Date
NixReaper 489755136c chore: bump version to 0.4.1, update CHANGELOG 2026-04-11 09:05:14 -05:00
NixReaper 2911cab755 fix(install): enable proxy_http and proxy_wstunnel Apache modules
Without proxy_http, ProxyPass to HTTP backends silently fails and Apache
returns 500 for all API requests. Without proxy_wstunnel, the WebSocket
RewriteRule for live stats cannot proxy ws:// connections.
2026-04-11 09:01:31 -05:00
NixReaper 8d6fe60168 fix(install): set world-readable permissions on frontend dist dirs
Apache (www-data) couldn't traverse /opt/nixpanel/nixserver because the
directories lacked execute permission for other. Add an explicit chmod
step after the build to ensure all dist directories are 755 and files
are 644 so Apache can serve the static assets.
2026-04-11 08:53:47 -05:00
NixReaper 6e59552e4f fix(install): handle pre-existing install dir during git clone
The 'Creating directory structure' step runs mkdir -p on INSTALL_DIR
before the clone step, so git clone fails if the directory is non-empty.
Clone to a temp directory and copy contents into place to preserve any
existing data/ and logs/ subdirectories.
2026-04-11 08:31:42 -05:00
NixReaper eebcce027b fix(install): correct SpamAssassin service name and resolve PowerDNS port 53 conflict
On Ubuntu 24.04 the SpamAssassin service unit is 'spamd', not 'spamassassin'.
The old 'systemctl enable spamassassin' call exited with an error because the
unit file does not exist, killing the install due to set -euo pipefail.

Also moved the systemd-resolved stub listener disable to a dedicated step that
runs before the package installation step. Previously it ran inside the PowerDNS
config block — after apt had already tried (and failed) to restart pdns during
post-install because systemd-resolved was still holding port 53.
2026-04-11 08:19:49 -05:00
NixReaper 87edf902a1 fix: correct server port config and seed default settings
- config.ts: rename nixserverPort/nixclientPort to single port, default
  HOST to 127.0.0.1 (installer binds to loopback, Apache proxies)
- index.ts: use config.server.port
- seed.ts: fix wrong default settings (nginx->apache2, bind9->powerdns,
  postfix->exim4, vsftpd->disabled)
2026-04-11 08:05:26 -05:00
NixReaper d235d27fe3 fix: nixclient SSL calls correct user-scoped route, not admin endpoint
- Add /api/nixclient/ssl (GET) — returns certs for authenticated user only
- Add /api/nixclient/ssl/:id/renew (POST) — validates cert belongs to user
- Register sslRoutes in nixclient index.ts
- nixclient SSL.tsx was calling /nixserver/ssl (requireAdminOrReseller)
  which would always 401 for regular users — fixed to /nixclient/ssl
2026-04-11 05:39:01 -05:00
13 changed files with 106 additions and 26 deletions
+11
View File
@@ -6,6 +6,17 @@ Pre-1.0 versions treat MINOR as feature releases and PATCH as bug fixes.
---
## [0.4.1] — 2026-04-11
### Bug Fixes (Installer)
- **SpamAssassin service name**: On Ubuntu 24.04 the service unit is `spamd`, not `spamassassin`. The old `systemctl enable spamassassin` caused the install to abort due to `set -euo pipefail`.
- **PowerDNS port 53 conflict**: `systemd-resolved` stub listener now disabled in a dedicated step that runs *before* package installation. Previously it ran after apt had already tried (and failed) to restart `pdns` during post-install because port 53 was still held by `systemd-resolved`.
- **Git clone into pre-existing directory**: The directory structure step (`mkdir -p`) creates `$INSTALL_DIR` before the clone step, causing `git clone` to fail with "destination path already exists". Fixed by cloning to a temp directory and copying contents into place (preserving any existing `data/` and `logs/` subdirectories).
- **Apache 403 on frontend assets**: Frontend dist files were created with root-only permissions. Added a post-build step that sets 755/644 on all dist directories and files so Apache (`www-data`) can read them.
- **Apache 500 on all API requests**: `mod_proxy_http` was not enabled. Without it, `ProxyPass http://...` directives silently fail and Apache returns 500 for every API call. Also enabled `mod_proxy_wstunnel` (required for the `ws://` WebSocket reverse proxy).
---
## [0.4.0] — 2026-04-11
### New Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nixpanel/cpanel",
"version": "0.4.0",
"version": "0.4.1",
"private": true,
"type": "module",
"scripts": {
+2 -2
View File
@@ -10,14 +10,14 @@ export default function SSL() {
const fetch = async () => {
setLoading(true)
try { const r = await api.get('/nixserver/ssl'); setCerts(r.data.data) }
try { const r = await api.get('/nixclient/ssl'); setCerts(r.data.data) }
finally { setLoading(false) }
}
useEffect(() => { fetch() }, [])
const renew = async (id: number) => {
try { await api.post(`/nixserver/ssl/${id}/renew`); fetch() }
try { await api.post(`/nixclient/ssl/${id}/renew`); fetch() }
catch (err: unknown) { alert((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Renewal failed') }
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nixpanel/whm",
"version": "0.4.0",
"version": "0.4.1",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nixpanel-server",
"version": "0.4.0",
"version": "0.4.1",
"private": true,
"workspaces": [
"server",
+32 -10
View File
@@ -146,6 +146,18 @@ if ! step_done "nodejs"; then
fi
success "Node.js $(node --version) ready"
# ── Disable systemd-resolved stub listener (frees port 53 for PowerDNS) ─────
step "Freeing port 53 for PowerDNS"
if ! step_done "resolved_stub"; then
# Ubuntu 24.04 runs systemd-resolved with a stub listener on 127.0.0.53:53.
# PowerDNS's apt post-install restart fails if this is still active.
sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf 2>/dev/null || true
sed -i 's/^DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf 2>/dev/null || true
systemctl restart systemd-resolved 2>/dev/null || true
mark_done "resolved_stub"
fi
success "Port 53 freed"
# ── Install all services in one pass ─────────────────────────────────────────
step "Installing web stack (Apache, PHP, Exim, Dovecot, PowerDNS, MariaDB)"
if ! step_done "packages"; then
@@ -232,7 +244,7 @@ success "Directories created"
step "Configuring Apache"
if ! step_done "apache"; then
# Enable required modules
a2enmod rewrite ssl proxy proxy_fcgi headers expires deflate 2>/dev/null || true
a2enmod rewrite ssl proxy proxy_http proxy_wstunnel proxy_fcgi headers expires deflate 2>/dev/null || true
# Disable default vhost
a2dissite 000-default 2>/dev/null || true
@@ -344,10 +356,6 @@ setuid=pdns
setgid=pdns
PDNS
# Disable systemd-resolved on port 53 to avoid conflict
sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf 2>/dev/null || true
systemctl restart systemd-resolved 2>/dev/null || true
systemctl enable pdns
systemctl start pdns
@@ -431,10 +439,9 @@ success "Dovecot configured"
# ── Configure SpamAssassin ────────────────────────────────────────────────────
step "Configuring SpamAssassin"
if ! step_done "spamassassin"; then
sed -i 's/ENABLED=0/ENABLED=1/' /etc/default/spamassassin 2>/dev/null || true
sed -i 's/CRON=0/CRON=1/' /etc/default/spamassassin 2>/dev/null || true
systemctl enable spamassassin
systemctl start spamassassin
# Ubuntu 24.04: service unit is 'spamd', not 'spamassassin'
systemctl enable spamd
systemctl start spamd
mark_done "spamassassin"
fi
success "SpamAssassin configured"
@@ -475,7 +482,12 @@ if [[ -d "$INSTALL_DIR/.git" ]]; then
git -C "$INSTALL_DIR" pull --ff-only
else
info "Cloning repository into $INSTALL_DIR..."
git clone https://github.com/NixReaper/nixpanel-server.git "$INSTALL_DIR"
# The directory may already exist (created by the mkdir step above).
# Clone to a temp dir then copy contents in so data/ and logs/ are preserved.
TMP_CLONE=$(mktemp -d)
git clone https://github.com/NixReaper/nixpanel-server.git "$TMP_CLONE"
cp -a "$TMP_CLONE/." "$INSTALL_DIR/"
rm -rf "$TMP_CLONE"
fi
success "NixPanel source ready at $INSTALL_DIR"
@@ -565,6 +577,16 @@ cd "$INSTALL_DIR/server"
npm run build --silent
success "Build complete"
# ── Set web-readable permissions on frontend assets ───────────────────────────
step "Setting frontend file permissions"
# Apache runs as www-data — ensure it can traverse parents and read dist files
chmod 755 "$INSTALL_DIR" "$INSTALL_DIR/nixserver" "$INSTALL_DIR/nixclient"
find "$INSTALL_DIR/nixserver/dist" -type d -exec chmod 755 {} \;
find "$INSTALL_DIR/nixserver/dist" -type f -exec chmod 644 {} \;
find "$INSTALL_DIR/nixclient/dist" -type d -exec chmod 755 {} \;
find "$INSTALL_DIR/nixclient/dist" -type f -exec chmod 644 {} \;
success "File permissions set"
# ── systemd service ───────────────────────────────────────────────────────────
step "Creating systemd service"
cat > /etc/systemd/system/nixpanel.service <<UNIT
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nixpanel/server",
"version": "0.4.0",
"version": "0.4.1",
"private": true,
"type": "module",
"main": "dist/index.js",
+1 -1
View File
@@ -73,7 +73,7 @@ export async function buildApp() {
fastify.get('/api/health', async () => ({
status: 'ok',
version: '0.3.2',
version: '0.4.1',
timestamp: new Date().toISOString(),
}))
+2 -3
View File
@@ -15,9 +15,8 @@ export const config = {
isDev: optional('NODE_ENV', 'development') === 'development',
server: {
host: optional('HOST', '0.0.0.0'),
nixserverPort: parseInt(optional('NIXSERVER_PORT', '4000'), 10),
nixclientPort: parseInt(optional('NIXCLIENT_PORT', '4001'), 10),
host: optional('HOST', '127.0.0.1'),
port: parseInt(optional('PORT', '4000'), 10),
},
db: {
+4 -4
View File
@@ -73,10 +73,10 @@ async function main() {
// Default settings
const settings = [
{ key: 'php_default_version', value: '8.3' },
{ key: 'mail_server', value: 'postfix' },
{ key: 'ftp_server', value: 'vsftpd' },
{ key: 'webserver', value: 'nginx' },
{ key: 'dns_server', value: 'bind9' },
{ key: 'mail_server', value: 'exim4' },
{ key: 'ftp_server', value: 'disabled' },
{ key: 'webserver', value: 'apache2' },
{ key: 'dns_server', value: 'powerdns' },
{ key: 'ssl_provider', value: 'letsencrypt' },
{ key: 'backup_enabled', value: 'true' },
{ key: 'backup_retention_days', value: '7' },
+2 -2
View File
@@ -13,8 +13,8 @@ async function start() {
app = await buildApp()
await app.listen({ port: config.server.nixserverPort, host: config.server.host })
console.log(`[nixpanel] API running on ${config.server.host}:${config.server.nixserverPort}`)
await app.listen({ port: config.server.port, host: config.server.host })
console.log(`[nixpanel] API running on ${config.server.host}:${config.server.port}`)
startScheduler()
+2
View File
@@ -6,6 +6,7 @@ import domainRoutes from './domains.js'
import ftpRoutes from './ftp.js'
import cronRoutes from './cron.js'
import statsRoutes from './stats.js'
import sslRoutes from './ssl.js'
export default async function nixclientRoutes(fastify: FastifyInstance) {
fastify.register(fileRoutes, { prefix: '/files' })
@@ -15,4 +16,5 @@ export default async function nixclientRoutes(fastify: FastifyInstance) {
fastify.register(ftpRoutes, { prefix: '/ftp' })
fastify.register(cronRoutes, { prefix: '/cron' })
fastify.register(statsRoutes, { prefix: '/stats' })
fastify.register(sslRoutes, { prefix: '/ssl' })
}
+46
View File
@@ -0,0 +1,46 @@
import type { FastifyInstance } from 'fastify'
import prisma from '../../db/client.js'
import { requireAuth } from '../../middleware/auth.js'
import { exec } from '../../core/exec.js'
export default async function sslRoutes(fastify: FastifyInstance) {
const preHandler = [requireAuth]
// GET /api/nixclient/ssl — certs for the authenticated user's account only
fastify.get('/', { preHandler }, async (request, reply) => {
const user = request.user!
const accountId = user.sub
const certs = await prisma.sslCertificate.findMany({
where: { accountId },
orderBy: { createdAt: 'desc' },
})
return reply.send({ success: true, data: certs })
})
// POST /api/nixclient/ssl/:id/renew
fastify.post('/:id/renew', { preHandler }, async (request, reply) => {
const { id } = request.params as { id: string }
const user = request.user!
const cert = await prisma.sslCertificate.findFirst({
where: { id: parseInt(id, 10), accountId: user.sub },
})
if (!cert) return reply.code(404).send({ success: false, error: 'Certificate not found' })
const result = await exec('certbot', ['renew', '--cert-name', cert.domain, '--non-interactive'])
if (result.exitCode !== 0) {
return reply.code(500).send({ success: false, error: `Renewal failed: ${result.stderr}` })
}
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 90)
const updated = await prisma.sslCertificate.update({
where: { id: cert.id },
data: { status: 'active', issuedAt: new Date(), expiresAt },
})
return reply.send({ success: true, data: updated })
})
}