Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
489755136c | ||
|
|
2911cab755 | ||
|
|
8d6fe60168 | ||
|
|
6e59552e4f | ||
|
|
eebcce027b | ||
|
|
87edf902a1 | ||
|
|
d235d27fe3 |
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nixpanel/cpanel",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nixpanel/whm",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nixpanel-server",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"server",
|
||||
|
||||
+32
-10
@@ -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
@@ -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
@@ -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(),
|
||||
}))
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user