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:
@@ -8,6 +8,25 @@ Versions follow [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `nixpanel-accounts` section binary — full account provisioner
|
||||||
|
- Creates Apache vhost with PHP-FPM proxy
|
||||||
|
- Creates PHP-FPM pool (per-user socket, open_basedir isolation)
|
||||||
|
- Creates PowerDNS zone with A, www, mail, ftp, MX, SPF records
|
||||||
|
- Builds home directory structure: `public_html/`, `logs/`, `tmp/`, `mail/`, `backups/`
|
||||||
|
- Writes default `index.html` for new domains
|
||||||
|
- Sets correct ownership and permissions
|
||||||
|
- `deprovision` action removes vhost, FPM pool, DNS zone
|
||||||
|
- NixClient real JWT authentication — login now calls `/api/auth/login`
|
||||||
|
- Session persists across refresh via `localStorage`
|
||||||
|
- Admin accounts redirected to NixServer (port 2087) instead of being allowed in
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- NixClient login accepted any username/password (was using fake `setTimeout` bypass)
|
||||||
|
- NixClient Sign out button now clears JWT and session correctly
|
||||||
|
- `CreateAccount` `FormField` component moved to module level — fixes focus jumping to next field after one keystroke (React remount bug from component defined inside render function)
|
||||||
|
- `BIGINT UNSIGNED` columns changed to `BIGINT` in DB schema — fixes `i64` type mismatch error on login
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
- DNS zone editor (CRUD records via PowerDNS API)
|
- DNS zone editor (CRUD records via PowerDNS API)
|
||||||
- Email account management (Dovecot mailbox provisioning)
|
- Email account management (Dovecot mailbox provisioning)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"nixpanel-common",
|
"nixpanel-common",
|
||||||
"nixpanel-api",
|
"nixpanel-api",
|
||||||
|
"nixpanel-accounts",
|
||||||
"nixpanel-services",
|
"nixpanel-services",
|
||||||
"nixpanel-sysinfo",
|
"nixpanel-sysinfo",
|
||||||
]
|
]
|
||||||
|
|||||||
+38
-14
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import * as api from './api'
|
||||||
|
|
||||||
/* ── Types ──────────────────────────────────────────────────────────────── */
|
/* ── Types ──────────────────────────────────────────────────────────────── */
|
||||||
type Page =
|
type Page =
|
||||||
@@ -304,14 +305,11 @@ const PAGE_META: Record<Page, { title: string; icon: string }> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Client panel shell ─────────────────────────────────────────────────── */
|
/* ── Client panel shell ─────────────────────────────────────────────────── */
|
||||||
function ClientPanel() {
|
function ClientPanel({ onLogout }: { onLogout: () => void }) {
|
||||||
const [page, setPage] = useState<Page>('home')
|
const [page, setPage] = useState<Page>('home')
|
||||||
const [loggedIn, setLoggedIn] = useState(true)
|
|
||||||
|
|
||||||
if (!loggedIn) return null
|
|
||||||
|
|
||||||
const meta = PAGE_META[page]
|
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() {
|
function renderPage() {
|
||||||
if (page === 'home') return <Home onNav={setPage} />
|
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>
|
<span className="text-xs text-gray-300 font-medium max-w-[120px] truncate">{domain}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
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
|
Sign out
|
||||||
@@ -404,11 +402,19 @@ function ClientPanel() {
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
const [loggedInAs, setLoggedInAs] = useState<string | null>(null)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
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()
|
e.preventDefault()
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
setError('Please enter your username and password.')
|
setError('Please enter your username and password.')
|
||||||
@@ -416,13 +422,31 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
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)
|
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 (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-[#0f1520] relative overflow-hidden">
|
<div className="min-h-screen flex items-center justify-center bg-[#0f1520] relative overflow-hidden">
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "nixpanel-accounts"
|
||||||
|
version = "0.2.0-alpha"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "nixpanel-accounts"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nixpanel-common = { path = "../nixpanel-common" }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
//! nixpanel-accounts — account provisioning section binary
|
||||||
|
//!
|
||||||
|
//! Actions:
|
||||||
|
//! provision {username, domain, email, server_ip, php_version?}
|
||||||
|
//! → creates vhost, PHP-FPM pool, DNS zone, home directory structure
|
||||||
|
//!
|
||||||
|
//! deprovision {username, domain}
|
||||||
|
//! → removes vhost, PHP-FPM pool, DNS zone, optionally home dir
|
||||||
|
|
||||||
|
use nixpanel_common::{SectionRequest, SectionResponse};
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{self, BufRead};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/* ── helpers ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
fn run(prog: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
let out = Command::new(prog)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to run {}: {}", prog, e))?;
|
||||||
|
if out.status.success() {
|
||||||
|
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||||
|
} else {
|
||||||
|
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ok(prog: &str, args: &[&str]) {
|
||||||
|
let _ = Command::new(prog).args(args).output();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file(path: &str, content: &str) -> Result<(), String> {
|
||||||
|
fs::write(path, content).map_err(|e| format!("Write {}: {}", path, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_ip() -> String {
|
||||||
|
// Read from /opt/nixpanel sysinfo, or fall back to hostname -I
|
||||||
|
Command::new("hostname")
|
||||||
|
.arg("-I")
|
||||||
|
.output()
|
||||||
|
.map(|o| {
|
||||||
|
String::from_utf8_lossy(&o.stdout)
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("127.0.0.1")
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Apache vhost ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
fn create_vhost(username: &str, domain: &str, php_version: &str) -> Result<(), String> {
|
||||||
|
let public_html = format!("/home/{}/public_html", username);
|
||||||
|
let log_dir = format!("/home/{}/logs", username);
|
||||||
|
let sock = format!("/run/php/php{}-fpm-{}.sock", php_version, username);
|
||||||
|
|
||||||
|
let conf = format!(
|
||||||
|
r#"# NixPanel — {domain} ({username})
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName {domain}
|
||||||
|
ServerAlias www.{domain}
|
||||||
|
DocumentRoot {public_html}
|
||||||
|
|
||||||
|
<Directory {public_html}>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<FilesMatch \.php$>
|
||||||
|
SetHandler "proxy:unix:{sock}|fcgi://localhost"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
ErrorLog {log_dir}/error.log
|
||||||
|
CustomLog {log_dir}/access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
"#,
|
||||||
|
domain = domain,
|
||||||
|
username = username,
|
||||||
|
public_html = public_html,
|
||||||
|
log_dir = log_dir,
|
||||||
|
sock = sock,
|
||||||
|
);
|
||||||
|
|
||||||
|
let conf_path = format!("/etc/apache2/sites-available/{}.conf", username);
|
||||||
|
write_file(&conf_path, &conf)?;
|
||||||
|
|
||||||
|
run("a2ensite", &[&format!("{}.conf", username)])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_vhost(username: &str) {
|
||||||
|
run_ok("a2dissite", &[&format!("{}.conf", username)]);
|
||||||
|
let _ = fs::remove_file(format!("/etc/apache2/sites-available/{}.conf", username));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── PHP-FPM pool ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
fn create_fpm_pool(username: &str, php_version: &str) -> Result<(), String> {
|
||||||
|
let sock = format!("/run/php/php{}-fpm-{}.sock", php_version, username);
|
||||||
|
let pool = format!(
|
||||||
|
r#"[{username}]
|
||||||
|
user = {username}
|
||||||
|
group = {username}
|
||||||
|
|
||||||
|
listen = {sock}
|
||||||
|
listen.owner = www-data
|
||||||
|
listen.group = www-data
|
||||||
|
listen.mode = 0660
|
||||||
|
|
||||||
|
pm = ondemand
|
||||||
|
pm.max_children = 10
|
||||||
|
pm.process_idle_timeout = 10s
|
||||||
|
pm.max_requests = 500
|
||||||
|
|
||||||
|
php_admin_value[open_basedir] = /home/{username}/:/tmp/
|
||||||
|
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
|
||||||
|
|
||||||
|
env[PATH] = /usr/local/bin:/usr/bin:/bin
|
||||||
|
"#,
|
||||||
|
username = username,
|
||||||
|
sock = sock,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pool_path = format!(
|
||||||
|
"/etc/php/{}/fpm/pool.d/{}.conf",
|
||||||
|
php_version, username
|
||||||
|
);
|
||||||
|
write_file(&pool_path, &pool)?;
|
||||||
|
|
||||||
|
run("systemctl", &["reload", &format!("php{}-fpm", php_version)])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_fpm_pool(username: &str, php_version: &str) {
|
||||||
|
let path = format!("/etc/php/{}/fpm/pool.d/{}.conf", php_version, username);
|
||||||
|
let _ = fs::remove_file(&path);
|
||||||
|
run_ok("systemctl", &["reload", &format!("php{}-fpm", php_version)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── PowerDNS zone ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
fn create_dns_zone(domain: &str, ip: &str) -> Result<(), String> {
|
||||||
|
// Create zone
|
||||||
|
run("pdnsutil", &["create-zone", domain])?;
|
||||||
|
|
||||||
|
// A records
|
||||||
|
run("pdnsutil", &["add-record", domain, "@", "A", "3600", ip])?;
|
||||||
|
run("pdnsutil", &["add-record", domain, "www", "A", "3600", ip])?;
|
||||||
|
run("pdnsutil", &["add-record", domain, "mail","A", "3600", ip])?;
|
||||||
|
run("pdnsutil", &["add-record", domain, "ftp", "A", "3600", ip])?;
|
||||||
|
|
||||||
|
// MX record
|
||||||
|
run("pdnsutil", &["add-record", domain, "@", "MX", "3600",
|
||||||
|
&format!("10 mail.{}.", domain)])?;
|
||||||
|
|
||||||
|
// SPF TXT
|
||||||
|
run("pdnsutil", &["add-record", domain, "@", "TXT", "3600",
|
||||||
|
&format!("\"v=spf1 a mx ip4:{} ~all\"", ip)])?;
|
||||||
|
|
||||||
|
// Increase serial and notify
|
||||||
|
run_ok("pdnsutil", &["rectify-zone", domain]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_dns_zone(domain: &str) {
|
||||||
|
run_ok("pdnsutil", &["delete-zone", domain]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Home directory structure ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
fn create_home(username: &str, domain: &str) -> Result<(), String> {
|
||||||
|
let dirs = [
|
||||||
|
format!("/home/{}/public_html", username),
|
||||||
|
format!("/home/{}/logs", username),
|
||||||
|
format!("/home/{}/tmp", username),
|
||||||
|
format!("/home/{}/mail", username),
|
||||||
|
format!("/home/{}/backups", username),
|
||||||
|
];
|
||||||
|
|
||||||
|
for dir in &dirs {
|
||||||
|
fs::create_dir_all(dir)
|
||||||
|
.map_err(|e| format!("mkdir {}: {}", dir, e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default index.html
|
||||||
|
let index = format!(
|
||||||
|
r#"<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8"><title>Welcome to {domain}</title>
|
||||||
|
<style>body{{font-family:sans-serif;text-align:center;padding:80px;color:#333}}
|
||||||
|
h1{{color:#e85d04}}p{{color:#666}}</style></head>
|
||||||
|
<body>
|
||||||
|
<h1>🚀 {domain}</h1>
|
||||||
|
<p>Your hosting account is active. Upload your files to <code>public_html</code> to get started.</p>
|
||||||
|
<p><small>Powered by NixPanel</small></p>
|
||||||
|
</body></html>
|
||||||
|
"#,
|
||||||
|
domain = domain
|
||||||
|
);
|
||||||
|
write_file(&format!("/home/{}/public_html/index.html", username), &index)?;
|
||||||
|
|
||||||
|
// Set ownership to system user
|
||||||
|
run("chown", &["-R", &format!("{}:{}", username, username),
|
||||||
|
&format!("/home/{}", username)])?;
|
||||||
|
run("chmod", &["750", &format!("/home/{}", username)])?;
|
||||||
|
run("chmod", &["755", &format!("/home/{}/public_html", username)])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Actions ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
fn action_provision(params: &serde_json::Value) -> SectionResponse {
|
||||||
|
let username = params["username"].as_str().unwrap_or("");
|
||||||
|
let domain = params["domain"].as_str().unwrap_or("");
|
||||||
|
let php_version = params["php_version"].as_str().unwrap_or("8.3");
|
||||||
|
let ip = params["server_ip"].as_str()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_else(server_ip);
|
||||||
|
|
||||||
|
if username.is_empty() || domain.is_empty() {
|
||||||
|
return SectionResponse::err("username and domain are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitise: only allow alphanumeric + hyphen/dot in domain,
|
||||||
|
// alphanumeric + underscore in username
|
||||||
|
if !username.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
|
||||||
|
return SectionResponse::err("Invalid username");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut steps: Vec<String> = Vec::new();
|
||||||
|
let mut errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// 1. Home directory
|
||||||
|
match create_home(username, domain) {
|
||||||
|
Ok(()) => steps.push("home directory created".into()),
|
||||||
|
Err(e) => errors.push(format!("home dir: {}", e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. PHP-FPM pool
|
||||||
|
match create_fpm_pool(username, php_version) {
|
||||||
|
Ok(()) => steps.push(format!("PHP {} FPM pool created", php_version)),
|
||||||
|
Err(e) => errors.push(format!("fpm pool: {}", e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Apache vhost
|
||||||
|
match create_vhost(username, domain, php_version) {
|
||||||
|
Ok(()) => steps.push("Apache vhost created".into()),
|
||||||
|
Err(e) => errors.push(format!("vhost: {}", e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Reload Apache
|
||||||
|
match run("systemctl", &["reload", "apache2"]) {
|
||||||
|
Ok(_) => steps.push("Apache reloaded".into()),
|
||||||
|
Err(e) => errors.push(format!("apache reload: {}", e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. PowerDNS zone
|
||||||
|
match create_dns_zone(domain, &ip) {
|
||||||
|
Ok(()) => steps.push(format!("DNS zone created (IP: {})", ip)),
|
||||||
|
Err(e) => errors.push(format!("DNS: {}", e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
SectionResponse::ok(serde_json::json!({
|
||||||
|
"steps": steps,
|
||||||
|
"domain": domain,
|
||||||
|
"ip": ip,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// Partial success — return both what worked and what didn't
|
||||||
|
SectionResponse::ok(serde_json::json!({
|
||||||
|
"steps": steps,
|
||||||
|
"errors": errors,
|
||||||
|
"partial": true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_deprovision(params: &serde_json::Value) -> SectionResponse {
|
||||||
|
let username = params["username"].as_str().unwrap_or("");
|
||||||
|
let domain = params["domain"].as_str().unwrap_or("");
|
||||||
|
let php_version = params["php_version"].as_str().unwrap_or("8.3");
|
||||||
|
|
||||||
|
if username.is_empty() {
|
||||||
|
return SectionResponse::err("username is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_vhost(username);
|
||||||
|
remove_fpm_pool(username, php_version);
|
||||||
|
if !domain.is_empty() {
|
||||||
|
remove_dns_zone(domain);
|
||||||
|
}
|
||||||
|
run_ok("systemctl", &["reload", "apache2"]);
|
||||||
|
|
||||||
|
SectionResponse::ok(serde_json::json!({
|
||||||
|
"message": format!("Account {} deprovisioned", username)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Entry point ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let stdin = io::stdin();
|
||||||
|
let mut line = String::new();
|
||||||
|
stdin.lock().read_line(&mut line).unwrap_or(0);
|
||||||
|
|
||||||
|
let response = match serde_json::from_str::<SectionRequest>(line.trim()) {
|
||||||
|
Err(e) => SectionResponse::err(format!("Invalid JSON: {}", e)),
|
||||||
|
Ok(req) => match req.action.as_str() {
|
||||||
|
"provision" => action_provision(&req.params),
|
||||||
|
"deprovision" => action_deprovision(&req.params),
|
||||||
|
other => SectionResponse::err(format!("Unknown action: {}", other)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{}", serde_json::to_string(&response).unwrap());
|
||||||
|
}
|
||||||
@@ -61,19 +61,42 @@ pub async fn create_account(
|
|||||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("argon2: {}", e)))?
|
.map_err(|e| AppError::Internal(anyhow::anyhow!("argon2: {}", e)))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
// Create system user (best-effort)
|
// Create system user (must exist before provisioner runs)
|
||||||
let _ = std::process::Command::new("useradd")
|
let _ = std::process::Command::new("useradd")
|
||||||
.args(["-m", "-s", "/bin/bash", "-d",
|
.args(["-m", "-s", "/bin/bash", "-d",
|
||||||
&format!("/home/{}", body.username),
|
&format!("/home/{}", body.username),
|
||||||
&body.username])
|
&body.username])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let _ = std::fs::create_dir_all(format!("/home/{}/public_html", body.username));
|
|
||||||
|
|
||||||
let package_name = body.package_name.unwrap_or_else(|| "Default".into());
|
let package_name = body.package_name.unwrap_or_else(|| "Default".into());
|
||||||
let disk_quota_mb: i32 = body.disk_quota_mb.unwrap_or(10240);
|
let disk_quota_mb: i32 = body.disk_quota_mb.unwrap_or(10240);
|
||||||
let bandwidth_mb: i32 = body.bandwidth_mb.unwrap_or(0);
|
let bandwidth_mb: i32 = body.bandwidth_mb.unwrap_or(0);
|
||||||
|
|
||||||
|
// Run the full account provisioner (vhost, PHP-FPM, DNS zone, home dirs)
|
||||||
|
let provision_result = crate::section::call(
|
||||||
|
&state.config.bin_dir,
|
||||||
|
"nixpanel-accounts",
|
||||||
|
"provision",
|
||||||
|
serde_json::json!({
|
||||||
|
"username": body.username,
|
||||||
|
"domain": body.domain,
|
||||||
|
"email": body.email,
|
||||||
|
"php_version": "8.3",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(ref e) = provision_result {
|
||||||
|
tracing::warn!("Account provisioner error for {}: {}", body.username, e);
|
||||||
|
} else if let Ok(ref resp) = provision_result {
|
||||||
|
if let Some(errors) = resp.data.as_ref().and_then(|d| d.get("errors")) {
|
||||||
|
tracing::warn!("Provisioner partial errors for {}: {}", body.username, errors);
|
||||||
|
}
|
||||||
|
tracing::info!("Provisioner steps for {}: {:?}",
|
||||||
|
body.username,
|
||||||
|
resp.data.as_ref().and_then(|d| d.get("steps"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let insert_id: u64 = sqlx::query(
|
let insert_id: u64 = sqlx::query(
|
||||||
"INSERT INTO accounts (username, domain, email, password_hash, package_name, disk_quota_mb, bandwidth_mb, status)
|
"INSERT INTO accounts (username, domain, email, password_hash, package_name, disk_quota_mb, bandwidth_mb, status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')",
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')",
|
||||||
|
|||||||
Reference in New Issue
Block a user