- Created index.html for the main application interface with responsive design and dark mode support. - Added manifest.webmanifest for PWA configuration, including app icons and display settings. - Implemented service worker (sw.js) for offline caching of assets and network-first strategy for versioning. - Introduced version.json to manage app versioning.
259 lines
10 KiB
PHP
259 lines
10 KiB
PHP
<?php
|
||
// index.php – Basis-Menü + Login mit flexibler Config (YAML/JSON/INI)
|
||
session_start();
|
||
|
||
// ===== Pfade / Basis =====
|
||
$basePath = __DIR__;
|
||
$baseUrl = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\');
|
||
|
||
// ===== Config-Lader =====
|
||
function find_config_path(string $basePath): ?string {
|
||
// Reihenfolge der Dateitypen: YAML, YML, JSON, INI
|
||
$candidates = [
|
||
$basePath . '/config/config.yaml',
|
||
$basePath . '/config/config.yml',
|
||
$basePath . '/config/config.json',
|
||
$basePath . '/config/config.ini',
|
||
];
|
||
foreach ($candidates as $c) {
|
||
if (@is_file($c) && @is_readable($c)) return $c;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function parse_yaml_with_any(string $path): ?array {
|
||
// 1) ext-yaml?
|
||
if (function_exists('yaml_parse_file')) {
|
||
$cfg = @yaml_parse_file($path);
|
||
return is_array($cfg) ? $cfg : null;
|
||
}
|
||
// 2) Symfony/Yaml über Composer?
|
||
$autoload = __DIR__ . '/vendor/autoload.php';
|
||
if (is_file($autoload)) {
|
||
require_once $autoload;
|
||
if (class_exists(\Symfony\Component\Yaml\Yaml::class)) {
|
||
try {
|
||
$cfg = \Symfony\Component\Yaml\Yaml::parseFile($path);
|
||
return is_array($cfg) ? $cfg : null;
|
||
} catch (Throwable $e) {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function load_config(string $basePath): array {
|
||
$path = find_config_path($basePath);
|
||
if (!$path) {
|
||
http_response_code(500);
|
||
die("Config-Datei nicht gefunden. Erwartet: config.yaml|yml|json|ini in " . htmlspecialchars($basePath . '/config'));
|
||
}
|
||
|
||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||
$config = null;
|
||
|
||
if (in_array($ext, ['yaml', 'yml'], true)) {
|
||
$config = parse_yaml_with_any($path);
|
||
if ($config === null) {
|
||
// Fallback: Versuche JSON/INI wenn vorhanden (z. B. User hat nur eine davon)
|
||
// Aber hier ist die Datei YAML – wenn nicht parsebar, liefern wir einen klaren Fehler.
|
||
http_response_code(500);
|
||
die("YAML-Parser nicht verfügbar oder Datei ungültig: " . htmlspecialchars(basename($path)) . ". Installiere 'ext-yaml' oder 'symfony/yaml', oder nutze JSON/INI.");
|
||
}
|
||
} elseif ($ext === 'json') {
|
||
$raw = @file_get_contents($path);
|
||
if ($raw === false) {
|
||
http_response_code(500);
|
||
die("Config (JSON) konnte nicht gelesen werden: " . htmlspecialchars(basename($path)));
|
||
}
|
||
$config = json_decode($raw, true);
|
||
if (!is_array($config)) {
|
||
http_response_code(500);
|
||
die("Ungültige JSON-Config: " . htmlspecialchars(basename($path)));
|
||
}
|
||
} elseif ($ext === 'ini') {
|
||
$config = @parse_ini_file($path, true, INI_SCANNER_TYPED);
|
||
if (!is_array($config)) {
|
||
http_response_code(500);
|
||
die("Ungültige INI-Config: " . htmlspecialchars(basename($path)));
|
||
}
|
||
} else {
|
||
http_response_code(500);
|
||
die("Nicht unterstützte Config-Erweiterung: " . htmlspecialchars($ext));
|
||
}
|
||
|
||
// Normalisiere Struktur: wir erwarten auth.user / auth.pass
|
||
// Erlaube flach (user, pass) ODER verschachtelt (auth[user], auth[pass])
|
||
$user = $config['auth']['user'] ?? $config['user'] ?? null;
|
||
$pass = $config['auth']['pass'] ?? $config['pass'] ?? null;
|
||
|
||
if (!$user || !$pass) {
|
||
http_response_code(500);
|
||
die("Ungültige Config-Struktur. Erwartet z. B.:\n"
|
||
. "YAML/JSON:\n{\n \"auth\": {\"user\": \"Thomas\", \"pass\": \"Alles4Mich!\"}\n}\n"
|
||
. "INI:\n[auth]\nuser=Thomas\npass=Alles4Mich!");
|
||
}
|
||
|
||
return ['user' => (string)$user, 'pass' => (string)$pass, 'path' => $path];
|
||
}
|
||
|
||
$cfg = load_config($basePath);
|
||
$USERNAME = $cfg['user'];
|
||
$PASSWORD = $cfg['pass'];
|
||
|
||
// ===== Login / Logout Handling =====
|
||
if (isset($_GET['logout'])) {
|
||
$_SESSION = [];
|
||
if (ini_get("session.use_cookies")) {
|
||
$params = session_get_cookie_params();
|
||
setcookie(session_name(), '', time()-42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
|
||
}
|
||
session_destroy();
|
||
header("Location: ".$baseUrl."/");
|
||
exit;
|
||
}
|
||
|
||
$errors = [];
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
|
||
$u = $_POST['user'] ?? '';
|
||
$p = $_POST['pass'] ?? '';
|
||
// hash_equals verhindert Timing-Angriffe
|
||
if (hash_equals($USERNAME, $u) && hash_equals($PASSWORD, $p)) {
|
||
session_regenerate_id(true);
|
||
$_SESSION['auth'] = true;
|
||
header("Location: ".$baseUrl."/");
|
||
exit;
|
||
} else {
|
||
$errors[] = "Login fehlgeschlagen.";
|
||
}
|
||
}
|
||
|
||
$authed = ($_SESSION['auth'] ?? false) === true;
|
||
|
||
// ===== Verzeichnisse scannen =====
|
||
$apps = [];
|
||
if ($authed) {
|
||
foreach (glob($basePath . '/*', GLOB_ONLYDIR) as $dirPath) {
|
||
$name = basename($dirPath);
|
||
|
||
// Ordner, die NICHT im Menü erscheinen
|
||
if (in_array($name, ['vendor', '.git', '.well-known', 'config'], true)) continue;
|
||
|
||
$iconRel = $name . '/icon.png';
|
||
$iconAbs = $basePath . '/' . $iconRel;
|
||
$hasIcon = is_file($iconAbs);
|
||
|
||
$apps[] = [
|
||
'name' => $name,
|
||
'url' => ($baseUrl ? $baseUrl : '') . '/' . rawurlencode($name) . '/',
|
||
'icon' => $hasIcon ? (($baseUrl ? $baseUrl : '') . '/' . $iconRel . '?v=' . filemtime($iconAbs)) : null,
|
||
];
|
||
}
|
||
usort($apps, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>VIBING – Experimente</title>
|
||
<style>
|
||
:root { --bg:#0f1115; --card:#171a21; --fg:#e8eef9; --muted:#9aa4b2; --accent:#7aa2f7; }
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0; font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
|
||
background: radial-gradient(1200px 600px at 20% -10%, #182034, transparent), var(--bg); color: var(--fg);
|
||
min-height: 100svh; display: grid; place-items: center; padding: 24px;
|
||
}
|
||
.wrap { width: min(1100px, 100%); }
|
||
header { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; }
|
||
header h1 { margin:0; font-size: clamp(20px, 3vw, 28px); letter-spacing:.3px; }
|
||
header a.logout{ color: var(--muted); text-decoration:none; border:1px solid #2a3140; padding:8px 12px; border-radius:10px; }
|
||
.card {
|
||
background: linear-gradient(180deg, #1a1f2b, var(--card));
|
||
border: 1px solid #242b3a; border-radius: 16px; padding: clamp(16px, 3vw, 24px); box-shadow: 0 10px 30px rgba(0,0,0,.25);
|
||
}
|
||
form.login { display:grid; gap:12px; max-width:340px; }
|
||
label { font-size:14px; color: var(--muted); }
|
||
input[type=text], input[type=password] {
|
||
width:100%; padding:12px 14px; border-radius:10px; border:1px solid #2a3140; background:#0f1320; color:var(--fg);
|
||
}
|
||
button {
|
||
padding:12px 14px; border-radius:10px; border:1px solid #2e3650; background: #1f2740; color: var(--fg); cursor:pointer;
|
||
}
|
||
.error { color:#ff8b8b; margin:6px 0 0 0; font-size:14px; }
|
||
|
||
.grid { display:grid; gap:16px; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
|
||
.app { display:flex; flex-direction:column; gap:10px; padding:14px; border:1px solid #263147; border-radius:14px; background:#121726; text-decoration:none; color:inherit; }
|
||
.app:hover { border-color:#2f3c59; transform: translateY(-1px); transition: .15s ease; }
|
||
.icon {
|
||
width: 100%; aspect-ratio: 1/1; border-radius: 12px; background: #0d1220; display:grid; place-items:center;
|
||
overflow:hidden; border:1px solid #232b40;
|
||
}
|
||
.icon img { width:100%; height:100%; object-fit:cover; display:block; }
|
||
.fallback { font-size:38px; color: var(--accent); }
|
||
.name { font-weight:600; }
|
||
.empty { color: var(--muted); }
|
||
footer { margin-top:18px; color: var(--muted); font-size:13px; text-align:center; }
|
||
.hint { color: var(--muted); font-size: 12px; margin-top:8px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<header>
|
||
<h1>VIBING – Experimente</h1>
|
||
<?php if ($authed): ?>
|
||
<a class="logout" href="?logout=1">Logout</a>
|
||
<?php endif; ?>
|
||
</header>
|
||
|
||
<div class="card">
|
||
<?php if (!$authed): ?>
|
||
<form class="login" method="post" autocomplete="off">
|
||
<div>
|
||
<label for="user">Benutzername</label>
|
||
<input id="user" name="user" type="text" required>
|
||
</div>
|
||
<div>
|
||
<label for="pass">Passwort</label>
|
||
<input id="pass" name="pass" type="password" required>
|
||
</div>
|
||
<div>
|
||
<button name="login" value="1" type="submit">Login</button>
|
||
<?php foreach ($errors as $e): ?>
|
||
<div class="error"><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8') ?></div>
|
||
<?php endforeach; ?>
|
||
<div class="hint">
|
||
Unterstützte Config-Dateien: <code>config.yaml</code>, <code>config.yml</code>, <code>config.json</code>, <code>config.ini</code> im Ordner <code>config/</code>.
|
||
</div>
|
||
</div>
|
||
</form>
|
||
<?php else: ?>
|
||
<?php if (empty($apps)): ?>
|
||
<div class="empty">Noch keine Unterverzeichnisse gefunden. Lege Ordner auf Server-Ebene an (z. B. <code>/experiment1</code>) und optional ein <code>icon.png</code> hinein.</div>
|
||
<?php else: ?>
|
||
<div class="grid">
|
||
<?php foreach ($apps as $app): ?>
|
||
<a class="app" href="<?= htmlspecialchars($app['url'], ENT_QUOTES, 'UTF-8') ?>">
|
||
<div class="icon">
|
||
<?php if ($app['icon']): ?>
|
||
<img src="<?= htmlspecialchars($app['icon'], ENT_QUOTES, 'UTF-8') ?>" alt="icon">
|
||
<?php else: ?>
|
||
<div class="fallback">🧪</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="name"><?= htmlspecialchars($app['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<footer>Basis-Verzeichnis: <code><?= htmlspecialchars($baseUrl ?: '/', ENT_QUOTES, 'UTF-8') ?></code></footer>
|
||
</div>
|
||
</body>
|
||
</html>
|