Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da576bdbdc | ||
|
|
489755136c | ||
|
|
2911cab755 | ||
|
|
8d6fe60168 | ||
|
|
6e59552e4f | ||
|
|
eebcce027b | ||
|
|
87edf902a1 | ||
|
|
d235d27fe3 |
@@ -6,6 +6,24 @@ Pre-1.0 versions treat MINOR as feature releases and PATCH as bug fixes.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.4.2] — 2026-04-11
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- **NixServer sidebar redesigned** — replaced the flat 14-item nav with a full WHM-style collapsible category sidebar (29 categories, ~210 menu items). Categories fold/unfold on click; the active route's category auto-expands on navigation. A live search box at the top of the sidebar filters items across all categories and auto-expands matching results. Sidebar can be hidden entirely via the topbar toggle. All existing routes are wired to their correct menu items; future feature items route to the closest related hub page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [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
|
## [0.4.0] — 2026-04-11
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nixpanel/cpanel",
|
"name": "@nixpanel/cpanel",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ export default function SSL() {
|
|||||||
|
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
setLoading(true)
|
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) }
|
finally { setLoading(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { fetch() }, [])
|
useEffect(() => { fetch() }, [])
|
||||||
|
|
||||||
const renew = async (id: number) => {
|
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') }
|
catch (err: unknown) { alert((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Renewal failed') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nixpanel/whm",
|
"name": "@nixpanel/whm",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,33 +1,502 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useMemo, useEffect } from 'react'
|
||||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom'
|
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, Users, Package, UserCog, Globe, Mail, Shield,
|
LayoutDashboard, Settings, HelpCircle, Network, BookUser, UserCog,
|
||||||
Server, HardDrive, Settings, LogOut, Menu, X,
|
Wrench, Languages, HardDrive, GitFork, RefreshCw, Activity,
|
||||||
Activity, Lock, BarChart3, Cpu
|
Users, UserCheck, ArrowRightLeft, Paintbrush, Package,
|
||||||
|
Globe, Database, Wifi, Box, Mail, HeartPulse, Lock,
|
||||||
|
ShoppingBag, Code2, Puzzle, LogOut, Menu, X,
|
||||||
|
ChevronDown, ChevronRight, Search, RotateCcw
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
|
||||||
const nav = [
|
// ─── Nav data ────────────────────────────────────────────────────────────────
|
||||||
{ label: 'Dashboard', to: '/', icon: LayoutDashboard },
|
|
||||||
{ label: 'Accounts', to: '/accounts', icon: Users },
|
type NavItem = { label: string; to: string }
|
||||||
{ label: 'Packages', to: '/packages', icon: Package },
|
type NavCategory = {
|
||||||
{ label: 'Resellers', to: '/resellers', icon: UserCog },
|
id: string
|
||||||
{ label: 'DNS', to: '/dns', icon: Globe },
|
label: string
|
||||||
{ label: 'Email', to: '/email', icon: Mail },
|
icon: React.ComponentType<{ size?: number; className?: string }>
|
||||||
{ label: 'SSL', to: '/ssl', icon: Lock },
|
items: NavItem[]
|
||||||
{ label: 'PHP', to: '/php', icon: Cpu },
|
}
|
||||||
{ label: 'Web Server', to: '/webserver', icon: Server },
|
|
||||||
{ label: 'Services', to: '/services', icon: Activity },
|
const NAV: NavCategory[] = [
|
||||||
{ label: 'Backup', to: '/backup', icon: HardDrive },
|
{
|
||||||
{ label: 'Security', to: '/security', icon: Shield },
|
id: 'server-config',
|
||||||
{ label: 'System', to: '/system', icon: BarChart3 },
|
label: 'Server Configuration',
|
||||||
{ label: 'Settings', to: '/settings', icon: Settings },
|
icon: Settings,
|
||||||
|
items: [
|
||||||
|
{ label: 'Basic WebHost Manager® Setup', to: '/settings' },
|
||||||
|
{ label: 'Change Root Password', to: '/security' },
|
||||||
|
{ label: 'Configure Analytics', to: '/settings' },
|
||||||
|
{ label: 'Configure Cron Jobs', to: '/settings' },
|
||||||
|
{ label: 'Initial Quota Setup', to: '/settings' },
|
||||||
|
{ label: 'Link Server Nodes', to: '/settings' },
|
||||||
|
{ label: 'Server Profile', to: '/settings' },
|
||||||
|
{ label: 'Server Time', to: '/system' },
|
||||||
|
{ label: 'Statistics Software', to: '/settings' },
|
||||||
|
{ label: 'Terminal', to: '/system' },
|
||||||
|
{ label: 'Tweak Settings', to: '/settings' },
|
||||||
|
{ label: 'Update Preferences', to: '/settings' },
|
||||||
|
{ label: 'WHM Marketplace', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'support',
|
||||||
|
label: 'Support',
|
||||||
|
icon: HelpCircle,
|
||||||
|
items: [
|
||||||
|
{ label: 'Grant NixPanel Support Access', to: '/security' },
|
||||||
|
{ label: 'Support Center', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'networking',
|
||||||
|
label: 'Networking Setup',
|
||||||
|
icon: Network,
|
||||||
|
items: [
|
||||||
|
{ label: 'Change Hostname', to: '/security' },
|
||||||
|
{ label: 'Resolver Configuration', to: '/security' },
|
||||||
|
{ label: 'Security Center', to: '/security' },
|
||||||
|
{ label: 'Apache mod_userdir Tweak', to: '/webserver' },
|
||||||
|
{ label: 'SMTP Restrictions', to: '/email' },
|
||||||
|
{ label: 'Compiler Access', to: '/security' },
|
||||||
|
{ label: 'Configure Security Policies', to: '/security' },
|
||||||
|
{ label: 'mXSTOP Brute Force Protection', to: '/security' },
|
||||||
|
{ label: 'Host Access Control', to: '/security' },
|
||||||
|
{ label: 'Manage External Authentications', to: '/security' },
|
||||||
|
{ label: "Manage root's SSH Keys", to: '/security' },
|
||||||
|
{ label: 'Manage Wheel Group Users', to: '/security' },
|
||||||
|
{ label: 'ModSecurity™ Configuration', to: '/security' },
|
||||||
|
{ label: 'ModSecurity™ Tools', to: '/security' },
|
||||||
|
{ label: 'ModSecurity™ Vendors', to: '/security' },
|
||||||
|
{ label: 'Password Strength Configuration', to: '/security' },
|
||||||
|
{ label: 'Security Advisor', to: '/security' },
|
||||||
|
{ label: 'Security Questions', to: '/security' },
|
||||||
|
{ label: 'Shell Fork Bomb Protection', to: '/security' },
|
||||||
|
{ label: 'SSH Password Authorization Tweak', to: '/security' },
|
||||||
|
{ label: 'Traceroute Enable/Disable', to: '/security' },
|
||||||
|
{ label: 'Two-Factor Authentication', to: '/security' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'contacts',
|
||||||
|
label: 'Server Contacts',
|
||||||
|
icon: BookUser,
|
||||||
|
items: [
|
||||||
|
{ label: 'Contact Manager', to: '/settings' },
|
||||||
|
{ label: 'Edit System Mail Preferences', to: '/email' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resellers',
|
||||||
|
label: 'Resellers',
|
||||||
|
icon: UserCog,
|
||||||
|
items: [
|
||||||
|
{ label: 'Change Ownership of an Account', to: '/resellers' },
|
||||||
|
{ label: 'Change Ownership of Multiple Accounts', to: '/resellers' },
|
||||||
|
{ label: 'Edit Reseller Nameservers and Privileges', to: '/resellers' },
|
||||||
|
{ label: 'Email All Resellers', to: '/resellers' },
|
||||||
|
{ label: "Manage Reseller's IP Delegation", to: '/resellers' },
|
||||||
|
{ label: "Manage Reseller's Shared IP", to: '/resellers' },
|
||||||
|
{ label: 'Reseller Center', to: '/resellers' },
|
||||||
|
{ label: 'Reset Resellers', to: '/resellers' },
|
||||||
|
{ label: 'Show Reseller Accounts', to: '/resellers' },
|
||||||
|
{ label: 'View Reseller Usage', to: '/resellers' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'service-config',
|
||||||
|
label: 'Service Configuration',
|
||||||
|
icon: Wrench,
|
||||||
|
items: [
|
||||||
|
{ label: 'Apache Configuration', to: '/webserver' },
|
||||||
|
{ label: 'Log Rotation Configuration', to: '/settings' },
|
||||||
|
{ label: 'Web Disk Configuration', to: '/settings' },
|
||||||
|
{ label: 'Web Services Configuration', to: '/settings' },
|
||||||
|
{ label: 'Exim Configuration Manager', to: '/email' },
|
||||||
|
{ label: 'FTP Server Selection', to: '/settings' },
|
||||||
|
{ label: 'Mailserver Configuration', to: '/email' },
|
||||||
|
{ label: 'Manage Service SSL Certificates', to: '/ssl' },
|
||||||
|
{ label: 'Nameserver Selection', to: '/dns' },
|
||||||
|
{ label: 'Service Manager', to: '/services' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'locales',
|
||||||
|
label: 'Locales',
|
||||||
|
icon: Languages,
|
||||||
|
items: [
|
||||||
|
{ label: 'Configure Application Locales', to: '/settings' },
|
||||||
|
{ label: 'Copy a Locale', to: '/settings' },
|
||||||
|
{ label: 'Delete a Locale', to: '/settings' },
|
||||||
|
{ label: 'Edit a Locale', to: '/settings' },
|
||||||
|
{ label: 'Locale XML Download', to: '/settings' },
|
||||||
|
{ label: 'Locale XML Upload', to: '/settings' },
|
||||||
|
{ label: 'View Available Locales', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'backup',
|
||||||
|
label: 'Backup',
|
||||||
|
icon: HardDrive,
|
||||||
|
items: [
|
||||||
|
{ label: 'Backup Configuration', to: '/backup' },
|
||||||
|
{ label: 'Backup Restoration', to: '/backup' },
|
||||||
|
{ label: 'Backup User Selection', to: '/backup' },
|
||||||
|
{ label: 'File and Directory Restoration', to: '/backup' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'clusters',
|
||||||
|
label: 'Clusters',
|
||||||
|
icon: GitFork,
|
||||||
|
items: [
|
||||||
|
{ label: 'Configuration Cluster', to: '/settings' },
|
||||||
|
{ label: 'DNS Cluster', to: '/dns' },
|
||||||
|
{ label: 'Remote Access Key', to: '/security' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reboot',
|
||||||
|
label: 'System Reboot',
|
||||||
|
icon: RotateCcw,
|
||||||
|
items: [
|
||||||
|
{ label: 'Forceful Server Reboot', to: '/system' },
|
||||||
|
{ label: 'Graceful Server Reboot', to: '/system' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'server-status',
|
||||||
|
label: 'Server Status',
|
||||||
|
icon: Activity,
|
||||||
|
items: [
|
||||||
|
{ label: 'Apache Status', to: '/webserver' },
|
||||||
|
{ label: 'Daily Process Log', to: '/system' },
|
||||||
|
{ label: 'Server Information',to: '/system' },
|
||||||
|
{ label: 'Service Status', to: '/services' },
|
||||||
|
{ label: 'Task Queue Monitor',to: '/system' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'account-info',
|
||||||
|
label: 'Account Information',
|
||||||
|
icon: Users,
|
||||||
|
items: [
|
||||||
|
{ label: 'List Accounts', to: '/accounts/list' },
|
||||||
|
{ label: 'List Parked Domains', to: '/accounts/parked-domains'},
|
||||||
|
{ label: 'List Subdomains', to: '/accounts/subdomains' },
|
||||||
|
{ label: 'List Suspended Accounts', to: '/accounts/suspended' },
|
||||||
|
{ label: 'Show Accounts Over Quota', to: '/accounts/over-quota' },
|
||||||
|
{ label: 'View Bandwidth Usage', to: '/accounts/bandwidth' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'account-functions',
|
||||||
|
label: 'Account Functions',
|
||||||
|
icon: UserCheck,
|
||||||
|
items: [
|
||||||
|
{ label: "Change Site's IP Address", to: '/accounts/change-ip' },
|
||||||
|
{ label: 'Create a New Account', to: '/accounts/create' },
|
||||||
|
{ label: 'Email All Users', to: '/accounts/email-users' },
|
||||||
|
{ label: 'Force Password Change', to: '/accounts/force-password' },
|
||||||
|
{ label: 'Limit Bandwidth Usage', to: '/accounts/limit-bandwidth' },
|
||||||
|
{ label: 'Manage Account Suspension', to: '/accounts/suspension' },
|
||||||
|
{ label: 'Manage Demo Mode', to: '/accounts/demo-mode' },
|
||||||
|
{ label: 'Manage Shell Access', to: '/accounts/shell-access' },
|
||||||
|
{ label: 'Modify an Account', to: '/accounts/modify' },
|
||||||
|
{ label: 'Password Modification', to: '/accounts/password' },
|
||||||
|
{ label: 'Quota Modification', to: '/accounts/quota' },
|
||||||
|
{ label: 'Raw Apache Log Download', to: '/accounts/apache-logs' },
|
||||||
|
{ label: 'Raw NGINX® Log Download', to: '/accounts/nginx-logs' },
|
||||||
|
{ label: 'Rearrange an Account', to: '/accounts/rearrange' },
|
||||||
|
{ label: 'Reset Account Bandwidth', to: '/accounts/reset-bandwidth' },
|
||||||
|
{ label: 'Terminate Accounts', to: '/accounts/terminate' },
|
||||||
|
{ label: 'Unsuspend Bandwidth Exceeders', to: '/accounts/unsuspend-bandwidth' },
|
||||||
|
{ label: 'Upgrade/Downgrade an Account', to: '/accounts/upgrade' },
|
||||||
|
{ label: 'Web Template Editor', to: '/accounts/web-template' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'multi-account',
|
||||||
|
label: 'Multi Account Functions',
|
||||||
|
icon: Users,
|
||||||
|
items: [
|
||||||
|
{ label: "Change Multiple Sites' IP Addresses", to: '/accounts/bulk-ip' },
|
||||||
|
{ label: 'Modify/Upgrade Multiple Accounts', to: '/accounts/bulk-modify' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transfers',
|
||||||
|
label: 'Transfers',
|
||||||
|
icon: ArrowRightLeft,
|
||||||
|
items: [
|
||||||
|
{ label: 'Convert Addon Domain to Account', to: '/accounts' },
|
||||||
|
{ label: 'Review Transfers and Restores', to: '/accounts' },
|
||||||
|
{ label: 'Transfer or Restore a cPanel Account', to: '/accounts' },
|
||||||
|
{ label: 'Transfer Tool', to: '/accounts' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'themes',
|
||||||
|
label: 'Themes',
|
||||||
|
icon: Paintbrush,
|
||||||
|
items: [
|
||||||
|
{ label: 'Theme Manager', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'packages',
|
||||||
|
label: 'Packages',
|
||||||
|
icon: Package,
|
||||||
|
items: [
|
||||||
|
{ label: 'Add a Package', to: '/packages' },
|
||||||
|
{ label: 'Delete a Package', to: '/packages' },
|
||||||
|
{ label: 'Edit a Package', to: '/packages' },
|
||||||
|
{ label: 'Feature Manager', to: '/packages' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dns',
|
||||||
|
label: 'DNS Functions',
|
||||||
|
icon: Globe,
|
||||||
|
items: [
|
||||||
|
{ label: 'Add a DNS Zone', to: '/dns' },
|
||||||
|
{ label: 'Add an A Entry for Hostname', to: '/dns' },
|
||||||
|
{ label: 'Delete a DNS Zone', to: '/dns' },
|
||||||
|
{ label: 'DNS Zone Manager', to: '/dns' },
|
||||||
|
{ label: 'Edit Zone Templates', to: '/dns' },
|
||||||
|
{ label: 'Email DNS Record Manager', to: '/dns' },
|
||||||
|
{ label: 'Email Routing Configuration', to: '/email' },
|
||||||
|
{ label: 'Enable DKIM/SPF Globally', to: '/email' },
|
||||||
|
{ label: 'Nameserver Record Report', to: '/dns' },
|
||||||
|
{ label: 'Park a Domain', to: '/dns' },
|
||||||
|
{ label: 'Perform a DNS Cleanup', to: '/dns' },
|
||||||
|
{ label: 'Set Zone TTL', to: '/dns' },
|
||||||
|
{ label: 'Setup/Edit Domain Forwarding',to: '/dns' },
|
||||||
|
{ label: 'Synchronize DNS Records', to: '/dns' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'databases',
|
||||||
|
label: 'Database Services',
|
||||||
|
icon: Database,
|
||||||
|
items: [
|
||||||
|
{ label: 'Change Database Root Password', to: '/settings' },
|
||||||
|
{ label: 'Change Database User Password', to: '/settings' },
|
||||||
|
{ label: 'Database Map Tool', to: '/settings' },
|
||||||
|
{ label: 'Edit Database Configuration', to: '/settings' },
|
||||||
|
{ label: 'Manage Database Access Hosts', to: '/settings' },
|
||||||
|
{ label: 'Manage Database Profiles', to: '/settings' },
|
||||||
|
{ label: 'Manage Database Users', to: '/settings' },
|
||||||
|
{ label: 'Manage Databases', to: '/settings' },
|
||||||
|
{ label: 'phpMyAdmin', to: '/settings' },
|
||||||
|
{ label: 'Repair Databases', to: '/settings' },
|
||||||
|
{ label: 'Show Database Processes', to: '/settings' },
|
||||||
|
{ label: 'Upgrade Database Version', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ip',
|
||||||
|
label: 'IP Functions',
|
||||||
|
icon: Wifi,
|
||||||
|
items: [
|
||||||
|
{ label: 'IPv6 Ranges', to: '/security' },
|
||||||
|
{ label: 'Add a New IP Address', to: '/security' },
|
||||||
|
{ label: 'Assign IPv6 Address', to: '/security' },
|
||||||
|
{ label: 'Configure Remote Service IPs', to: '/security' },
|
||||||
|
{ label: 'IP Migration Wizard', to: '/security' },
|
||||||
|
{ label: 'Rebuild the IP Address Pool', to: '/security' },
|
||||||
|
{ label: 'Show IP Address Usage', to: '/security' },
|
||||||
|
{ label: 'Show or Delete Current IPs', to: '/security' },
|
||||||
|
{ label: 'Show/Edit Reserved IPs', to: '/security' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'software',
|
||||||
|
label: 'Software',
|
||||||
|
icon: Box,
|
||||||
|
items: [
|
||||||
|
{ label: 'Install a Perl Module', to: '/system' },
|
||||||
|
{ label: 'Install Distro Packages', to: '/system' },
|
||||||
|
{ label: 'Module Installers', to: '/system' },
|
||||||
|
{ label: 'MultiPHP INI Editor', to: '/php' },
|
||||||
|
{ label: 'MultiPHP Manager', to: '/php' },
|
||||||
|
{ label: 'NGINX® Manager', to: '/webserver' },
|
||||||
|
{ label: 'Rebuild RPM Database', to: '/system' },
|
||||||
|
{ label: 'System Update', to: '/system' },
|
||||||
|
{ label: 'Update Server Software', to: '/system' },
|
||||||
|
{ label: 'EasyApache 4', to: '/webserver' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
icon: Mail,
|
||||||
|
items: [
|
||||||
|
{ label: 'Email Deliverability', to: '/email' },
|
||||||
|
{ label: 'Filter Incoming Emails by Country', to: '/email' },
|
||||||
|
{ label: 'Filter Incoming Emails by Domain', to: '/email' },
|
||||||
|
{ label: 'Greylisting', to: '/email' },
|
||||||
|
{ label: 'Mail Delivery Reports', to: '/email' },
|
||||||
|
{ label: 'Mail Queue Manager', to: '/email' },
|
||||||
|
{ label: 'Mail Troubleshooter', to: '/email' },
|
||||||
|
{ label: 'Mailbox Conversion', to: '/email' },
|
||||||
|
{ label: 'Repair Mailbox Permissions', to: '/email' },
|
||||||
|
{ label: 'Spamd Startup Configuration', to: '/email' },
|
||||||
|
{ label: 'View Mail Statistics Summary', to: '/email' },
|
||||||
|
{ label: 'View Relayers', to: '/email' },
|
||||||
|
{ label: 'View Sent Summary', to: '/email' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'system-health',
|
||||||
|
label: 'System Health',
|
||||||
|
icon: HeartPulse,
|
||||||
|
items: [
|
||||||
|
{ label: 'Background Process Killer', to: '/system' },
|
||||||
|
{ label: 'Process Manager', to: '/system' },
|
||||||
|
{ label: 'Show Current Disk Usage', to: '/system' },
|
||||||
|
{ label: 'Show Current Running Processes', to: '/system' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nixpanel',
|
||||||
|
label: 'NixPanel',
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
items: [
|
||||||
|
{ label: 'Change Log', to: '/settings' },
|
||||||
|
{ label: 'Customization', to: '/settings' },
|
||||||
|
{ label: 'Manage Plugins', to: '/settings' },
|
||||||
|
{ label: 'Modify NixPanel News', to: '/settings' },
|
||||||
|
{ label: 'Reset a Mailman Password', to: '/email' },
|
||||||
|
{ label: 'Upgrade to Latest Version', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ssl',
|
||||||
|
label: 'SSL/TLS',
|
||||||
|
icon: Lock,
|
||||||
|
items: [
|
||||||
|
{ label: 'Generate an SSL Certificate and Signing Request', to: '/ssl' },
|
||||||
|
{ label: 'Install an SSL Certificate on a Domain', to: '/ssl' },
|
||||||
|
{ label: 'Manage AutoSSL', to: '/ssl' },
|
||||||
|
{ label: 'Manage SSL Hosts', to: '/ssl' },
|
||||||
|
{ label: 'Purchase and Install an SSL Certificate', to: '/ssl' },
|
||||||
|
{ label: 'SSL Storage Manager', to: '/ssl' },
|
||||||
|
{ label: 'SSL/TLS Configuration', to: '/ssl' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'market',
|
||||||
|
label: 'Market',
|
||||||
|
icon: ShoppingBag,
|
||||||
|
items: [
|
||||||
|
{ label: 'Market Provider Manager', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'restart-services',
|
||||||
|
label: 'Restart Services',
|
||||||
|
icon: RefreshCw,
|
||||||
|
items: [
|
||||||
|
{ label: 'Database Server', to: '/services' },
|
||||||
|
{ label: 'DNS Server', to: '/services' },
|
||||||
|
{ label: 'HTTP Server (Apache)', to: '/services' },
|
||||||
|
{ label: 'IMAP Server', to: '/services' },
|
||||||
|
{ label: 'Mail Server (Exim)', to: '/services' },
|
||||||
|
{ label: 'Mailing List Manager (Mailman)', to: '/services' },
|
||||||
|
{ label: 'PHP-FPM service for Apache', to: '/services' },
|
||||||
|
{ label: 'SSH Server (OpenSSH)', to: '/services' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'development',
|
||||||
|
label: 'Development',
|
||||||
|
icon: Code2,
|
||||||
|
items: [
|
||||||
|
{ label: 'Apps Managed by AppConfig', to: '/settings' },
|
||||||
|
{ label: 'NixPanel Development Forum', to: '/settings' },
|
||||||
|
{ label: 'NixPanel Plugin File Generator', to: '/settings' },
|
||||||
|
{ label: 'Developer Documentation', to: '/settings' },
|
||||||
|
{ label: 'Manage API Tokens', to: '/settings' },
|
||||||
|
{ label: 'Manage Hooks', to: '/settings' },
|
||||||
|
{ label: 'OpenAPI Documentation for NixPanel', to: '/settings' },
|
||||||
|
{ label: 'OpenAPI Documentation for WHM', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'plugins',
|
||||||
|
label: 'Plugins',
|
||||||
|
icon: Puzzle,
|
||||||
|
items: [
|
||||||
|
{ label: 'Backuply by Softaculous', to: '/backup' },
|
||||||
|
{ label: 'ImunifyAV', to: '/security' },
|
||||||
|
{ label: 'Server Monitoring', to: '/system' },
|
||||||
|
{ label: 'SitePad Website Builder', to: '/settings' },
|
||||||
|
{ label: 'Softaculous - Instant Installs', to: '/settings' },
|
||||||
|
{ label: 'WP Toolkit', to: '/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function isItemActive(to: string, pathname: string) {
|
||||||
|
if (to === '/') return pathname === '/'
|
||||||
|
return pathname === to || pathname.startsWith(to + '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [openCats, setOpenCats] = useState<Set<string>>(() => {
|
||||||
|
// Auto-open the category that contains the active route on first render
|
||||||
|
const active = NAV.find(cat =>
|
||||||
|
cat.items.some(item => isItemActive(item.to, location.pathname))
|
||||||
|
)
|
||||||
|
return active ? new Set([active.id]) : new Set()
|
||||||
|
})
|
||||||
|
|
||||||
|
// When the route changes (e.g. user navigates), auto-open the matching category
|
||||||
|
useEffect(() => {
|
||||||
|
const active = NAV.find(cat =>
|
||||||
|
cat.items.some(item => isItemActive(item.to, location.pathname))
|
||||||
|
)
|
||||||
|
if (active) {
|
||||||
|
setOpenCats(prev => prev.has(active.id) ? prev : new Set([...prev, active.id]))
|
||||||
|
}
|
||||||
|
}, [location.pathname])
|
||||||
|
|
||||||
|
const toggleCat = (id: string) => {
|
||||||
|
setOpenCats(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.has(id) ? next.delete(id) : next.add(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter nav based on search query
|
||||||
|
const filteredNav = useMemo(() => {
|
||||||
|
const q = search.trim().toLowerCase()
|
||||||
|
if (!q) return NAV
|
||||||
|
return NAV.map(cat => ({
|
||||||
|
...cat,
|
||||||
|
items: cat.items.filter(item => item.label.toLowerCase().includes(q)),
|
||||||
|
})).filter(cat =>
|
||||||
|
cat.items.length > 0 || cat.label.toLowerCase().includes(q)
|
||||||
|
)
|
||||||
|
}, [search])
|
||||||
|
|
||||||
|
// When searching, all matching categories are forced open
|
||||||
|
const effectiveOpen = (id: string) =>
|
||||||
|
search.trim() ? filteredNav.some(c => c.id === id) : openCats.has(id)
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
@@ -36,68 +505,139 @@ export default function Layout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-[#0f1117]">
|
<div className="flex h-screen overflow-hidden bg-[#0f1117]">
|
||||||
{/* Sidebar */}
|
|
||||||
|
{/* ── Sidebar ─────────────────────────────────────────────────────── */}
|
||||||
<aside
|
<aside
|
||||||
className={`flex flex-col transition-all duration-200 bg-[#1a1d27] border-r border-[#2a2d3e] ${
|
className={`flex flex-col transition-all duration-200 bg-[#1a1d27] border-r border-[#2a2d3e] overflow-hidden flex-shrink-0 ${
|
||||||
sidebarOpen ? 'w-56' : 'w-14'
|
sidebarOpen ? 'w-72' : 'w-0'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Logo */}
|
{/* Branding */}
|
||||||
<div className="flex items-center gap-2 px-4 py-4 border-b border-[#2a2d3e] min-h-[57px]">
|
<div className="flex items-center gap-2 px-4 py-4 border-b border-[#2a2d3e] min-h-[57px] flex-shrink-0">
|
||||||
<div className="w-7 h-7 rounded-lg bg-indigo-600 flex items-center justify-center flex-shrink-0">
|
<div className="w-7 h-7 rounded-lg bg-indigo-600 flex items-center justify-center flex-shrink-0">
|
||||||
<span className="text-white text-xs font-bold">N</span>
|
<span className="text-white text-xs font-bold">N</span>
|
||||||
</div>
|
</div>
|
||||||
{sidebarOpen && (
|
<div className="overflow-hidden">
|
||||||
<div className="overflow-hidden">
|
<p className="text-white text-sm font-semibold leading-tight whitespace-nowrap">NixServer</p>
|
||||||
<p className="text-white text-sm font-semibold leading-tight">NixServer</p>
|
<p className="text-[#64748b] text-xs whitespace-nowrap">Admin Panel</p>
|
||||||
<p className="text-[#64748b] text-xs">Admin Panel</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="px-3 py-2 border-b border-[#2a2d3e] flex-shrink-0">
|
||||||
|
<div className="relative">
|
||||||
|
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#4a5568] pointer-events-none" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="w-full bg-[#0f1117] border border-[#2a2d3e] rounded-md text-xs text-[#94a3b8] placeholder-[#4a5568] pl-7 pr-3 py-1.5 focus:outline-none focus:border-indigo-500 transition-colors"
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearch('')}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-[#4a5568] hover:text-[#94a3b8]"
|
||||||
|
>
|
||||||
|
<X size={11} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav */}
|
{/* Nav */}
|
||||||
<nav className="flex-1 overflow-y-auto py-2">
|
<nav className="flex-1 overflow-y-auto py-1 scrollbar-thin">
|
||||||
{nav.map(({ label, to, icon: Icon }) => (
|
|
||||||
<NavLink
|
{/* Dashboard — always at the top */}
|
||||||
key={to}
|
<Link
|
||||||
to={to}
|
to="/"
|
||||||
end={to === '/'}
|
className={`flex items-center gap-2.5 px-3 py-1.5 mx-2 my-0.5 rounded-md text-xs transition-colors ${
|
||||||
className={({ isActive }) =>
|
location.pathname === '/'
|
||||||
`flex items-center gap-3 px-4 py-2 mx-2 my-0.5 rounded-lg text-sm transition-colors ${
|
? 'bg-indigo-600/20 text-indigo-400'
|
||||||
isActive
|
: 'text-[#94a3b8] hover:bg-[#1e2130] hover:text-white'
|
||||||
? 'bg-indigo-600/20 text-indigo-400'
|
}`}
|
||||||
: 'text-[#94a3b8] hover:bg-[#1e2130] hover:text-white'
|
>
|
||||||
}`
|
<LayoutDashboard size={13} className="flex-shrink-0" />
|
||||||
}
|
<span className="truncate">Dashboard</span>
|
||||||
>
|
</Link>
|
||||||
<Icon size={16} className="flex-shrink-0" />
|
|
||||||
{sidebarOpen && <span className="truncate">{label}</span>}
|
{/* Categories */}
|
||||||
</NavLink>
|
{filteredNav.map(cat => {
|
||||||
))}
|
const isOpen = effectiveOpen(cat.id)
|
||||||
|
const Icon = cat.icon
|
||||||
|
const hasActive = cat.items.some(item => isItemActive(item.to, location.pathname))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={cat.id} className="mx-2 my-0.5">
|
||||||
|
{/* Category header */}
|
||||||
|
<button
|
||||||
|
onClick={() => !search.trim() && toggleCat(cat.id)}
|
||||||
|
className={`flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||||
|
hasActive && !search.trim()
|
||||||
|
? 'text-indigo-300'
|
||||||
|
: 'text-[#64748b] hover:text-[#94a3b8]'
|
||||||
|
} ${search.trim() ? 'cursor-default' : ''}`}
|
||||||
|
>
|
||||||
|
<Icon size={13} className="flex-shrink-0" />
|
||||||
|
<span className="flex-1 text-left truncate">{cat.label}</span>
|
||||||
|
{!search.trim() && (
|
||||||
|
isOpen
|
||||||
|
? <ChevronDown size={11} className="flex-shrink-0 opacity-60" />
|
||||||
|
: <ChevronRight size={11} className="flex-shrink-0 opacity-60" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Items */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="ml-3 pl-2 border-l border-[#2a2d3e] mt-0.5 mb-1">
|
||||||
|
{cat.items.map(item => {
|
||||||
|
const active = isItemActive(item.to, location.pathname)
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.label}
|
||||||
|
to={item.to}
|
||||||
|
className={`block px-2 py-1 rounded text-xs transition-colors truncate ${
|
||||||
|
active
|
||||||
|
? 'text-indigo-400 bg-indigo-600/10'
|
||||||
|
: 'text-[#64748b] hover:text-[#94a3b8] hover:bg-[#1e2130]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Empty search state */}
|
||||||
|
{search.trim() && filteredNav.length === 0 && (
|
||||||
|
<p className="text-center text-[#4a5568] text-xs py-6">No results</p>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* User + logout */}
|
{/* User + logout */}
|
||||||
<div className="border-t border-[#2a2d3e] p-3">
|
<div className="border-t border-[#2a2d3e] p-3 flex-shrink-0">
|
||||||
{sidebarOpen && (
|
<div className="px-1 mb-2">
|
||||||
<div className="px-1 mb-2">
|
<p className="text-white text-xs font-medium truncate">{user?.username}</p>
|
||||||
<p className="text-white text-xs font-medium truncate">{user?.username}</p>
|
<p className="text-[#64748b] text-xs truncate">{user?.role}</p>
|
||||||
<p className="text-[#64748b] text-xs truncate">{user?.role}</p>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="flex items-center gap-2 px-3 py-2 w-full rounded-lg text-[#94a3b8] hover:bg-[#1e2130] hover:text-red-400 transition-colors text-sm"
|
className="flex items-center gap-2 px-3 py-2 w-full rounded-lg text-[#94a3b8] hover:bg-[#1e2130] hover:text-red-400 transition-colors text-xs"
|
||||||
>
|
>
|
||||||
<LogOut size={16} className="flex-shrink-0" />
|
<LogOut size={14} className="flex-shrink-0" />
|
||||||
{sidebarOpen && <span>Logout</span>}
|
<span>Logout</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main */}
|
{/* ── Main ────────────────────────────────────────────────────────── */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0">
|
||||||
{/* Topbar */}
|
{/* Topbar */}
|
||||||
<header className="flex items-center gap-4 px-6 py-3 bg-[#1a1d27] border-b border-[#2a2d3e] min-h-[57px]">
|
<header className="flex items-center gap-4 px-6 py-3 bg-[#1a1d27] border-b border-[#2a2d3e] min-h-[57px] flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen(v => !v)}
|
onClick={() => setSidebarOpen(v => !v)}
|
||||||
className="text-[#64748b] hover:text-white transition-colors"
|
className="text-[#64748b] hover:text-white transition-colors"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nixpanel-server",
|
"name": "nixpanel-server",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"server",
|
"server",
|
||||||
|
|||||||
+32
-10
@@ -146,6 +146,18 @@ if ! step_done "nodejs"; then
|
|||||||
fi
|
fi
|
||||||
success "Node.js $(node --version) ready"
|
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 ─────────────────────────────────────────
|
# ── Install all services in one pass ─────────────────────────────────────────
|
||||||
step "Installing web stack (Apache, PHP, Exim, Dovecot, PowerDNS, MariaDB)"
|
step "Installing web stack (Apache, PHP, Exim, Dovecot, PowerDNS, MariaDB)"
|
||||||
if ! step_done "packages"; then
|
if ! step_done "packages"; then
|
||||||
@@ -232,7 +244,7 @@ success "Directories created"
|
|||||||
step "Configuring Apache"
|
step "Configuring Apache"
|
||||||
if ! step_done "apache"; then
|
if ! step_done "apache"; then
|
||||||
# Enable required modules
|
# 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
|
# Disable default vhost
|
||||||
a2dissite 000-default 2>/dev/null || true
|
a2dissite 000-default 2>/dev/null || true
|
||||||
@@ -344,10 +356,6 @@ setuid=pdns
|
|||||||
setgid=pdns
|
setgid=pdns
|
||||||
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 enable pdns
|
||||||
systemctl start pdns
|
systemctl start pdns
|
||||||
|
|
||||||
@@ -431,10 +439,9 @@ success "Dovecot configured"
|
|||||||
# ── Configure SpamAssassin ────────────────────────────────────────────────────
|
# ── Configure SpamAssassin ────────────────────────────────────────────────────
|
||||||
step "Configuring SpamAssassin"
|
step "Configuring SpamAssassin"
|
||||||
if ! step_done "spamassassin"; then
|
if ! step_done "spamassassin"; then
|
||||||
sed -i 's/ENABLED=0/ENABLED=1/' /etc/default/spamassassin 2>/dev/null || true
|
# Ubuntu 24.04: service unit is 'spamd', not 'spamassassin'
|
||||||
sed -i 's/CRON=0/CRON=1/' /etc/default/spamassassin 2>/dev/null || true
|
systemctl enable spamd
|
||||||
systemctl enable spamassassin
|
systemctl start spamd
|
||||||
systemctl start spamassassin
|
|
||||||
mark_done "spamassassin"
|
mark_done "spamassassin"
|
||||||
fi
|
fi
|
||||||
success "SpamAssassin configured"
|
success "SpamAssassin configured"
|
||||||
@@ -475,7 +482,12 @@ if [[ -d "$INSTALL_DIR/.git" ]]; then
|
|||||||
git -C "$INSTALL_DIR" pull --ff-only
|
git -C "$INSTALL_DIR" pull --ff-only
|
||||||
else
|
else
|
||||||
info "Cloning repository into $INSTALL_DIR..."
|
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
|
fi
|
||||||
success "NixPanel source ready at $INSTALL_DIR"
|
success "NixPanel source ready at $INSTALL_DIR"
|
||||||
|
|
||||||
@@ -565,6 +577,16 @@ cd "$INSTALL_DIR/server"
|
|||||||
npm run build --silent
|
npm run build --silent
|
||||||
success "Build complete"
|
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 ───────────────────────────────────────────────────────────
|
# ── systemd service ───────────────────────────────────────────────────────────
|
||||||
step "Creating systemd service"
|
step "Creating systemd service"
|
||||||
cat > /etc/systemd/system/nixpanel.service <<UNIT
|
cat > /etc/systemd/system/nixpanel.service <<UNIT
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nixpanel/server",
|
"name": "@nixpanel/server",
|
||||||
"version": "0.4.0",
|
"version": "0.4.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ export async function buildApp() {
|
|||||||
|
|
||||||
fastify.get('/api/health', async () => ({
|
fastify.get('/api/health', async () => ({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
version: '0.3.2',
|
version: '0.4.2',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ export const config = {
|
|||||||
isDev: optional('NODE_ENV', 'development') === 'development',
|
isDev: optional('NODE_ENV', 'development') === 'development',
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
host: optional('HOST', '0.0.0.0'),
|
host: optional('HOST', '127.0.0.1'),
|
||||||
nixserverPort: parseInt(optional('NIXSERVER_PORT', '4000'), 10),
|
port: parseInt(optional('PORT', '4000'), 10),
|
||||||
nixclientPort: parseInt(optional('NIXCLIENT_PORT', '4001'), 10),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
db: {
|
db: {
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ async function main() {
|
|||||||
// Default settings
|
// Default settings
|
||||||
const settings = [
|
const settings = [
|
||||||
{ key: 'php_default_version', value: '8.3' },
|
{ key: 'php_default_version', value: '8.3' },
|
||||||
{ key: 'mail_server', value: 'postfix' },
|
{ key: 'mail_server', value: 'exim4' },
|
||||||
{ key: 'ftp_server', value: 'vsftpd' },
|
{ key: 'ftp_server', value: 'disabled' },
|
||||||
{ key: 'webserver', value: 'nginx' },
|
{ key: 'webserver', value: 'apache2' },
|
||||||
{ key: 'dns_server', value: 'bind9' },
|
{ key: 'dns_server', value: 'powerdns' },
|
||||||
{ key: 'ssl_provider', value: 'letsencrypt' },
|
{ key: 'ssl_provider', value: 'letsencrypt' },
|
||||||
{ key: 'backup_enabled', value: 'true' },
|
{ key: 'backup_enabled', value: 'true' },
|
||||||
{ key: 'backup_retention_days', value: '7' },
|
{ key: 'backup_retention_days', value: '7' },
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ async function start() {
|
|||||||
|
|
||||||
app = await buildApp()
|
app = await buildApp()
|
||||||
|
|
||||||
await app.listen({ port: config.server.nixserverPort, host: config.server.host })
|
await app.listen({ port: config.server.port, host: config.server.host })
|
||||||
console.log(`[nixpanel] API running on ${config.server.host}:${config.server.nixserverPort}`)
|
console.log(`[nixpanel] API running on ${config.server.host}:${config.server.port}`)
|
||||||
|
|
||||||
startScheduler()
|
startScheduler()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import domainRoutes from './domains.js'
|
|||||||
import ftpRoutes from './ftp.js'
|
import ftpRoutes from './ftp.js'
|
||||||
import cronRoutes from './cron.js'
|
import cronRoutes from './cron.js'
|
||||||
import statsRoutes from './stats.js'
|
import statsRoutes from './stats.js'
|
||||||
|
import sslRoutes from './ssl.js'
|
||||||
|
|
||||||
export default async function nixclientRoutes(fastify: FastifyInstance) {
|
export default async function nixclientRoutes(fastify: FastifyInstance) {
|
||||||
fastify.register(fileRoutes, { prefix: '/files' })
|
fastify.register(fileRoutes, { prefix: '/files' })
|
||||||
@@ -15,4 +16,5 @@ export default async function nixclientRoutes(fastify: FastifyInstance) {
|
|||||||
fastify.register(ftpRoutes, { prefix: '/ftp' })
|
fastify.register(ftpRoutes, { prefix: '/ftp' })
|
||||||
fastify.register(cronRoutes, { prefix: '/cron' })
|
fastify.register(cronRoutes, { prefix: '/cron' })
|
||||||
fastify.register(statsRoutes, { prefix: '/stats' })
|
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