feat: real account provisioner, fix NixClient auth bypass

- nixpanel-accounts: creates Apache vhost, PHP-FPM pool, PowerDNS
  zone (A/MX/SPF records), home dir structure, default index.html
- nixpanel-api: create_account now calls provisioner after DB insert
- NixClient: replace fake setTimeout login with real JWT API call;
  admin accounts blocked with redirect message; logout clears token
- CHANGELOG updated
This commit is contained in:
NixReaper
2026-04-25 21:32:32 -05:00
parent cb6be7c634
commit 389652b2e5
6 changed files with 420 additions and 18 deletions
+39 -15
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import * as api from './api'
/* ── Types ──────────────────────────────────────────────────────────────── */
type Page =
@@ -304,14 +305,11 @@ const PAGE_META: Record<Page, { title: string; icon: string }> = {
}
/* ── Client panel shell ─────────────────────────────────────────────────── */
function ClientPanel() {
function ClientPanel({ onLogout }: { onLogout: () => void }) {
const [page, setPage] = useState<Page>('home')
const [loggedIn, setLoggedIn] = useState(true)
if (!loggedIn) return null
const meta = PAGE_META[page]
const domain = 'yourdomain.com'
const username = localStorage.getItem('nixclient_username') ?? 'user'
const domain = username // domain matches account username for now
function renderPage() {
if (page === 'home') return <Home onNav={setPage} />
@@ -363,7 +361,7 @@ function ClientPanel() {
<span className="text-xs text-gray-300 font-medium max-w-[120px] truncate">{domain}</span>
</div>
<button
onClick={() => setLoggedIn(false)}
onClick={onLogout}
className="px-3 py-1.5 rounded-lg border border-[#2a3044] text-gray-400 hover:border-gray-600 hover:text-white transition-colors text-xs font-medium"
>
Sign out
@@ -404,11 +402,19 @@ function ClientPanel() {
export default function App() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [error, setError] = useState('')
const [loggedInAs, setLoggedInAs] = useState<string | null>(null)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleLogin = (e: React.FormEvent) => {
// Restore session on load
useEffect(() => {
if (api.isLoggedIn()) {
const stored = localStorage.getItem('nixclient_username')
setLoggedInAs(stored ?? 'user')
}
}, [])
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (!username || !password) {
setError('Please enter your username and password.')
@@ -416,13 +422,31 @@ export default function App() {
}
setLoading(true)
setError('')
setTimeout(() => {
try {
const result = await api.login(username, password)
// Only allow non-admin users into NixClient
if (result.user.role === 'admin') {
api.clearToken()
setError('Admin accounts must use NixServer (port 2087).')
return
}
localStorage.setItem('nixclient_username', result.user.username)
setLoggedInAs(result.user.username)
} catch (err: any) {
setError(err.message === 'Unauthorized' ? 'Invalid username or password.' : err.message)
} finally {
setLoading(false)
setIsLoggedIn(true)
}, 600)
}
}
if (isLoggedIn) return <ClientPanel />
const handleLogout = () => {
api.clearToken()
localStorage.removeItem('nixclient_username')
setLoggedInAs(null)
setUsername(''); setPassword('')
}
if (loggedInAs) return <ClientPanel onLogout={handleLogout} />
return (
<div className="min-h-screen flex items-center justify-center bg-[#0f1520] relative overflow-hidden">