commit
This commit is contained in:
@@ -1,41 +1,48 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// 1) Load config (constants, env, domains)
|
// 1. Autoloader für App Namespace
|
||||||
require_once __DIR__ . '/config.php';
|
spl_autoload_register(function ($class) {
|
||||||
|
$prefix = 'App\\';
|
||||||
|
$base_dir = __DIR__ . '/../src/App/';
|
||||||
|
|
||||||
// 2) Composer Autoloader (falls vorhanden)
|
$len = strlen($prefix);
|
||||||
$composerAutoload = __DIR__ . '/../vendor/autoload.php';
|
if (strncmp($prefix, $class, $len) !== 0) {
|
||||||
if (file_exists($composerAutoload)) {
|
// Versuche Repository Namespace
|
||||||
require_once $composerAutoload;
|
if (str_starts_with($class, 'App\\Repository\\')) {
|
||||||
} else {
|
$base_dir = __DIR__ . '/../src/Repository/';
|
||||||
// 2b) Fallback: minimaler Autoloader
|
$len = strlen('App\\Repository\\');
|
||||||
spl_autoload_register(function (string $class): void {
|
} else {
|
||||||
$prefix = 'App\\';
|
|
||||||
if (!str_starts_with($class, $prefix)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rel = substr($class, strlen($prefix));
|
$relative_class = substr($class, $len);
|
||||||
$path = __DIR__ . '/../src/App/' . str_replace('\\', '/', $rel) . '.php';
|
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||||
|
|
||||||
if (file_exists($path)) {
|
if (file_exists($file)) {
|
||||||
require_once $path;
|
require $file;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2. Funktionen laden
|
||||||
|
require_once __DIR__ . '/../src/App/functions.php';
|
||||||
|
|
||||||
|
// 3. Konfiguration laden
|
||||||
|
// Wir simulieren hier den Sync-Prozess: Wenn config/db.php nicht da ist, nimm staging.
|
||||||
|
$configFile = file_exists(__DIR__ . '/db.php') ? __DIR__ . '/db.php' : __DIR__ . '/staging/db.php';
|
||||||
|
$settingsFile = file_exists(__DIR__ . '/settings.php') ? __DIR__ . '/settings.php' : __DIR__ . '/staging/settings.php';
|
||||||
|
|
||||||
|
if (file_exists($settingsFile)) {
|
||||||
|
require_once $settingsFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Global helper functions (tpl(), t(), asset_*())
|
$dbConfig = [];
|
||||||
require_once __DIR__ . '/../src/helpers.php';
|
if (file_exists($configFile)) {
|
||||||
|
$dbConfig = require $configFile;
|
||||||
|
}
|
||||||
|
|
||||||
// 4) Initialize App (services)
|
// Globales Config Objekt erstellen
|
||||||
$config = \App\Config::fromPhpConstants(__DIR__ . '/../config');
|
global $appConfig;
|
||||||
\App\App::init($config);
|
$dbEnabled = defined('APP_DB_ENABLED') ? APP_DB_ENABLED : true;
|
||||||
|
$appConfig = new \App\Config($dbConfig, $dbEnabled);
|
||||||
// 5) Start session + create client-id cookie
|
|
||||||
$app = \App\App::get();
|
|
||||||
$app->session()->start();
|
|
||||||
$clientId = $app->session()->ensureClientId();
|
|
||||||
|
|
||||||
// Optionally expose a single global for templates if desired
|
|
||||||
$GLOBALS['client_id'] = $clientId;
|
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
|
use App\Database;
|
||||||
|
use App\Repository\KeaHostRepository;
|
||||||
|
|
||||||
echo "test";
|
global $appConfig;
|
||||||
|
|
||||||
|
$pdo = Database::createPdo($appConfig);
|
||||||
|
$hosts = [];
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
if ($pdo) {
|
||||||
|
try {
|
||||||
|
$repo = new KeaHostRepository($pdo);
|
||||||
|
$hosts = $repo->findAll(50);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$error = "Datenbankfehler: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = "Datenbankverbindung ist nicht konfiguriert oder deaktiviert.";
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl('kea_dashboard', 'landing', compact('hosts', 'error'));
|
||||||
74
public/page/kea_dashboard.php
Normal file
74
public/page/kea_dashboard.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var array $hosts Die Liste der KEA-Hosts.
|
||||||
|
* @var string|null $error Eine Fehlermeldung, falls vorhanden.
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-white">KEA DHCP Hosts</h1>
|
||||||
|
<button class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded shadow transition-colors">
|
||||||
|
+ Neuer Host
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="bg-red-900 border-l-4 border-red-500 text-red-100 p-4 mb-6" role="alert">
|
||||||
|
<p class="font-bold">Fehler</p>
|
||||||
|
<p><?= e($error) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="bg-gray-800 shadow overflow-hidden sm:rounded-lg border border-gray-700">
|
||||||
|
<div class="px-4 py-5 sm:px-6 border-b border-gray-700">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-200">
|
||||||
|
Registrierte Geräte
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 max-w-2xl text-sm text-gray-400">
|
||||||
|
Übersicht der statischen Reservierungen und bekannten Clients.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-700">
|
||||||
|
<thead class="bg-gray-900">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Hostname</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">IP Adresse</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">MAC Adresse</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Kontext</th>
|
||||||
|
<th scope="col" class="relative px-6 py-3">
|
||||||
|
<span class="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-gray-800 divide-y divide-gray-700">
|
||||||
|
<?php if (empty($hosts)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">Keine Hosts gefunden.</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($hosts as $host): ?>
|
||||||
|
<tr class="hover:bg-gray-750 transition-colors">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">
|
||||||
|
<?= e($host['hostname'] ?: 'Unbekannt') ?>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300 font-mono">
|
||||||
|
<?= e($host['ipv4_address']) ?>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400 font-mono">
|
||||||
|
<?= e($host['dhcp_identifier']) ?>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||||
|
<?= e($host['user_context'] ?? '-') ?>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href="#" class="text-indigo-400 hover:text-indigo-300">Bearbeiten</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
public/page/layout_end.php
Normal file
11
public/page/layout_end.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-gray-800 border-t border-gray-700 mt-auto">
|
||||||
|
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||||
|
<p class="text-center text-gray-500 text-xs">© <?= date('Y') ?> Nexus Control Panel. System Status: Nominal.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
38
public/page/layout_start.php
Normal file
38
public/page/layout_start.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" class="h-full bg-gray-900">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Nexus Control Panel</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
/* Custom Scrollbar für Nexus-Look */
|
||||||
|
::-webkit-scrollbar { width: 8px; }
|
||||||
|
::-webkit-scrollbar-track { background: #111827; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #374151; border-radius: 4px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #4B5563; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="h-full text-gray-300 font-sans antialiased flex flex-col">
|
||||||
|
|
||||||
|
<nav class="bg-gray-800 border-b border-gray-700">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0 text-indigo-500 font-bold text-xl tracking-wider">
|
||||||
|
NEXUS
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div class="ml-10 flex items-baseline space-x-4">
|
||||||
|
<a href="/" class="bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium">Dashboard</a>
|
||||||
|
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">DHCP Leases</a>
|
||||||
|
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Logs</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="flex-grow">
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
@@ -3,60 +3,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
final class Config
|
class Config
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $env,
|
public array $db,
|
||||||
public readonly string $prefix,
|
public bool $dbEnabled = true
|
||||||
public readonly string $primaryDomain,
|
) {
|
||||||
public readonly string $primaryUrl,
|
|
||||||
public readonly string $apiBase,
|
|
||||||
public readonly string $assetVersion,
|
|
||||||
public readonly bool $dbEnabled,
|
|
||||||
public readonly array $db,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static function fromPhpConstants(string $configDir): self
|
|
||||||
{
|
|
||||||
// config.php defines these constants.
|
|
||||||
$env = defined('APP_ENV') ? (string) APP_ENV : 'prod';
|
|
||||||
$prefix = defined('APP_PREFIX') ? (string) APP_PREFIX : 'app';
|
|
||||||
$primaryDom = defined('APP_DOMAIN_PRIMARY') ? (string) APP_DOMAIN_PRIMARY : 'example.test';
|
|
||||||
$primaryUrl = defined('APP_URL_PRIMARY') ? (string) APP_URL_PRIMARY : 'https://example.test';
|
|
||||||
$apiBase = defined('APP_API_BASE') ? (string) APP_API_BASE : ($primaryUrl . '/api');
|
|
||||||
$assetVersion = defined('ASSET_VERSION') ? (string) ASSET_VERSION : '';
|
|
||||||
|
|
||||||
$dbEnabled = defined('APP_DB_ENABLED') ? (bool) APP_DB_ENABLED : false;
|
|
||||||
|
|
||||||
$dbFileRoot = rtrim($configDir, '/\\') . DIRECTORY_SEPARATOR . 'db.php';
|
|
||||||
$dbFileEnv = rtrim($configDir, '/\\') . DIRECTORY_SEPARATOR . $env . DIRECTORY_SEPARATOR . 'db.php';
|
|
||||||
$dbFile = file_exists($dbFileRoot) ? $dbFileRoot : (file_exists($dbFileEnv) ? $dbFileEnv : null);
|
|
||||||
$db = $dbFile ? (array) require $dbFile : [];
|
|
||||||
|
|
||||||
return new self(
|
|
||||||
env: $env,
|
|
||||||
prefix: $prefix,
|
|
||||||
primaryDomain: $primaryDom,
|
|
||||||
primaryUrl: rtrim($primaryUrl, '/'),
|
|
||||||
apiBase: rtrim($apiBase, '/'),
|
|
||||||
assetVersion: $assetVersion,
|
|
||||||
dbEnabled: $dbEnabled,
|
|
||||||
db: $db
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cookiePrefix(): string
|
|
||||||
{
|
|
||||||
// Example: add suffix for staging
|
|
||||||
if ($this->env === 'staging') {
|
|
||||||
return $this->prefix . '_stg_';
|
|
||||||
}
|
|
||||||
return $this->prefix . '_';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cookieDomain(): string
|
|
||||||
{
|
|
||||||
// Leading dot for subdomain-wide cookies
|
|
||||||
return '.' . ltrim($this->primaryDomain, '.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
src/App/functions.php
Normal file
28
src/App/functions.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt ein Template-Partial.
|
||||||
|
*
|
||||||
|
* @param string $name Dateiname ohne .php
|
||||||
|
* @param string $folder Unterordner in /public/partials/
|
||||||
|
* @param array $data Daten, die im Template verfügbar sein sollen
|
||||||
|
*/
|
||||||
|
function tpl(string $name, string $folder = 'landing', array $data = []): void
|
||||||
|
{
|
||||||
|
$path = __DIR__ . '/../../public/partials/' . $folder . '/' . $name . '.php';
|
||||||
|
if (file_exists($path)) {
|
||||||
|
extract($data);
|
||||||
|
require $path;
|
||||||
|
} else {
|
||||||
|
echo "<!-- Template not found: {$folder}/{$name} -->";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML Escaping Helper.
|
||||||
|
*/
|
||||||
|
function e(?string $string): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
104
src/Repository/KeaHostRepository.php
Normal file
104
src/Repository/KeaHostRepository.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository für das KEA DHCP Host-Management.
|
||||||
|
* Interagiert direkt mit der 'hosts' Tabelle in der PostgreSQL-Datenbank.
|
||||||
|
*/
|
||||||
|
final class KeaHostRepository
|
||||||
|
{
|
||||||
|
public function __construct(private PDO $pdo) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruft eine Liste aller Host-Reservierungen ab (Geräte-Inventar).
|
||||||
|
*/
|
||||||
|
public function findAll(int $limit = 50): array
|
||||||
|
{
|
||||||
|
// 'dhcp_identifier' ist in KEA i.d.R. die MAC-Adresse (bei type=1)
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT host_id, dhcp_identifier, ipv4_address, hostname, user_context
|
||||||
|
FROM hosts
|
||||||
|
ORDER BY host_id DESC
|
||||||
|
LIMIT :limit'
|
||||||
|
);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sucht einen Host anhand der MAC-Adresse (dhcp_identifier).
|
||||||
|
*/
|
||||||
|
public function findByMac(string $mac): ?array
|
||||||
|
{
|
||||||
|
// Hinweis: KEA speichert MACs in PostgreSQL oft als BYTEA.
|
||||||
|
// Je nach Treiber-Konfiguration muss $mac hier ggf. als Hex-String (z.B. '\x...')
|
||||||
|
// formatiert übergeben werden.
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT host_id, dhcp_identifier, ipv4_address, hostname, user_context
|
||||||
|
FROM hosts
|
||||||
|
WHERE dhcp_identifier = :mac
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$stmt->execute(['mac' => $mac]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $row ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt eine neue Host-Reservierung inkl. Metadaten.
|
||||||
|
*
|
||||||
|
* @param string $mac MAC-Adresse
|
||||||
|
* @param string $ip IPv4-Adresse
|
||||||
|
* @param int $subnetId Die ID des Subnets (dhcp4_subnet_id), in dem die IP liegt
|
||||||
|
* @param string|null $hostname Optionaler Hostname
|
||||||
|
* @param array $metadata Zusätzliche Infos (Standort, Verantwortlicher etc.) für 'user_context'
|
||||||
|
*/
|
||||||
|
public function create(string $mac, string $ip, int $subnetId, ?string $hostname = null, array $metadata = []): int
|
||||||
|
{
|
||||||
|
// Metadaten werden im KEA-Standardfeld 'user_context' als JSON gespeichert
|
||||||
|
$userContextJson = json_encode($metadata, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
// dhcp_identifier_type 1 = HW_ADDRESS (Ethernet)
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'INSERT INTO hosts (dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address, hostname, user_context)
|
||||||
|
VALUES (:mac, 1, :subnetId, :ip, :hostname, :user_context)'
|
||||||
|
);
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'mac' => $mac,
|
||||||
|
'subnetId' => $subnetId,
|
||||||
|
'ip' => $ip,
|
||||||
|
'hostname' => $hostname,
|
||||||
|
'user_context' => $userContextJson,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->lastInsertIdSafe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function driver(): string
|
||||||
|
{
|
||||||
|
return (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function lastInsertIdSafe(): int
|
||||||
|
{
|
||||||
|
$driver = $this->driver();
|
||||||
|
|
||||||
|
if ($driver === 'pgsql') {
|
||||||
|
// KEA Standard-Sequenz für die hosts Tabelle
|
||||||
|
$id = $this->pdo->lastInsertId('hosts_host_id_seq');
|
||||||
|
if ($id !== '') {
|
||||||
|
return (int)$id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)$this->pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user