feat: add Rust backend — workspace with section executables

Cargo workspace with four crates:
- nixpanel-common: shared SectionRequest/SectionResponse types
- nixpanel-api: main Axum HTTP server (JWT auth, DB, routing)
- nixpanel-services: section binary for systemctl start/stop/restart
- nixpanel-sysinfo: section binary for CPU/RAM/disk/OS/version reads

nixpanel-api features:
- POST /api/auth/login — Argon2id verification, HS256 JWT
- GET  /api/dashboard  — real account counts + sysinfo + versions
- GET  /api/services   — live systemctl status via section binary
- POST /api/services/:name/:action — start/stop/restart
- GET  /api/accounts   — list from MariaDB
- POST /api/accounts   — create account + system user

Section binaries are UPX-compressed after build.
DB schema auto-migrated on first start; admin user seeded from
/opt/nixpanel/.admin_user + .admin_pass files.

Frontend changes:
- nixserver/src/api.ts + App.tsx wired to real API (JWT stored in
  localStorage, real data in dashboard/services/accounts)
- nixclient/src/api.ts added
- Both vite configs get /api proxy to localhost:4000 for dev
- install.sh updated to copy + compress all section binaries
This commit is contained in:
NixReaper
2026-04-25 16:41:57 -05:00
parent a257b5571c
commit 5247c77132
26 changed files with 1899 additions and 331 deletions
+366 -327
View File
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
/* NixServer API client — all calls go to /api/ (proxied to Rust backend) */
const BASE = '/api'
/* ── Auth token ──────────────────────────────────────────────────────── */
export function getToken(): string | null {
return localStorage.getItem('nixpanel_token')
}
export function setToken(t: string) {
localStorage.setItem('nixpanel_token', t)
}
export function clearToken() {
localStorage.removeItem('nixpanel_token')
}
export function isLoggedIn(): boolean {
return !!getToken()
}
/* ── Core fetch wrapper ──────────────────────────────────────────────── */
async function apiFetch<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const token = getToken()
const res = await fetch(`${BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.headers ?? {}),
},
})
if (res.status === 401) {
clearToken()
window.location.reload()
throw new Error('Unauthorized')
}
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new Error(body.error ?? `HTTP ${res.status}`)
}
return res.json()
}
/* ── Auth ────────────────────────────────────────────────────────────── */
export interface LoginResult {
token: string
user: { id: number; username: string; role: string }
}
export async function login(username: string, password: string): Promise<LoginResult> {
const result = await apiFetch<LoginResult>('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
})
setToken(result.token)
return result
}
/* ── Dashboard ───────────────────────────────────────────────────────── */
export interface DashboardStats {
accounts: number
domains: number
databases: number
email_accounts: number
}
export interface SysInfo {
hostname: string
ip: string
os: string
kernel: string
uptime_sec: number
cpu_pct: number
ram_used_mb: number
ram_total_mb: number
disk_used_gb: number
disk_total_gb: number
load_avg: [number, number, number]
}
export interface Versions {
apache: string
php: string
mariadb: string
nixpanel: string
}
export interface DashboardData {
stats: DashboardStats
sysinfo: SysInfo | null
versions: Versions | null
}
export async function getDashboard(): Promise<DashboardData> {
return apiFetch('/dashboard')
}
/* ── Services ────────────────────────────────────────────────────────── */
export interface ServiceInfo {
name: string
display: string
status: 'running' | 'stopped' | 'warning'
pid: number | null
uptime: string | null
}
export async function getServices(): Promise<ServiceInfo[]> {
return apiFetch('/services')
}
export async function serviceAction(
name: string,
action: 'start' | 'stop' | 'restart'
): Promise<{ success: boolean; message: string }> {
return apiFetch(`/services/${name}/${action}`, { method: 'POST' })
}
/* ── Accounts ────────────────────────────────────────────────────────── */
export interface Account {
id: number
username: string
domain: string
email: string
package_name: string
disk_quota_mb: number
bandwidth_mb: number
status: 'active' | 'suspended' | 'terminated'
created_at: string
}
export async function getAccounts(): Promise<Account[]> {
return apiFetch('/accounts')
}
export async function createAccount(data: {
username: string
domain: string
email: string
password: string
package_name?: string
disk_quota_mb?: number
bandwidth_mb?: number
}): Promise<Account> {
return apiFetch('/accounts', { method: 'POST', body: JSON.stringify(data) })
}
+6
View File
@@ -11,5 +11,11 @@ export default defineConfig({
server: {
port: 3000,
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:4000',
changeOrigin: true,
},
},
},
})