소스 검색

adding hosting on subfolder, configurable in config

Josef Straßl 1 개월 전
부모
커밋
f00679273e
9개의 변경된 파일85개의 추가작업 그리고 16개의 파일을 삭제
  1. 2 1
      data/config.json
  2. 19 1
      docs/CONFIG.md
  3. 12 6
      public/admin/index.php
  4. 1 1
      public/admin/logout.php
  5. 4 1
      public/app.js
  6. 9 5
      public/index.php
  7. 1 1
      public/openapi.yaml
  8. 1 0
      src/ConfigRepository.php
  9. 36 0
      src/bootstrap.php

+ 2 - 1
data/config.json

@@ -3,7 +3,8 @@
         "name": "Getraenkeautomat Monitor",
         "name": "Getraenkeautomat Monitor",
         "timezone": "Europe/Berlin",
         "timezone": "Europe/Berlin",
         "dashboard_refresh_seconds": 15,
         "dashboard_refresh_seconds": 15,
-        "default_from_email": "monitor@example.local"
+        "default_from_email": "monitor@example.local",
+        "base_path": ""
     },
     },
     "api": {
     "api": {
         "bearer_token": "demo-esp32-token"
         "bearer_token": "demo-esp32-token"

+ 19 - 1
docs/CONFIG.md

@@ -26,7 +26,8 @@ Beispiel:
   "name": "Getraenkeautomat Monitor",
   "name": "Getraenkeautomat Monitor",
   "timezone": "Europe/Berlin",
   "timezone": "Europe/Berlin",
   "dashboard_refresh_seconds": 15,
   "dashboard_refresh_seconds": 15,
-  "default_from_email": "monitor@example.local"
+  "default_from_email": "monitor@example.local",
+  "base_path": ""
 }
 }
 ```
 ```
 
 
@@ -40,6 +41,23 @@ Felder:
   - Polling-Intervall fuer das Dashboard
   - Polling-Intervall fuer das Dashboard
 - `default_from_email`
 - `default_from_email`
   - Absenderadresse fuer `mail()`
   - Absenderadresse fuer `mail()`
+- `base_path`
+  - optionaler URL-Pfad fuer Deployments unterhalb der Domain
+  - leer lassen fuer Betrieb direkt unter `/`
+  - Beispiel fuer `domain.de/auswertung/`: `"/auswertung"`
+  - fuehrende und abschliessende Slashes werden beim Speichern normalisiert
+
+Beispiel fuer einen Betrieb unter `https://domain.de/auswertung/`:
+
+```json
+{
+  "name": "Getraenkeautomat Monitor",
+  "timezone": "Europe/Berlin",
+  "dashboard_refresh_seconds": 15,
+  "default_from_email": "monitor@example.local",
+  "base_path": "/auswertung"
+}
+```
 
 
 ## Bereich `api`
 ## Bereich `api`
 
 

+ 12 - 6
public/admin/index.php

@@ -20,7 +20,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
         );
         );
 
 
         if ($success) {
         if ($success) {
-            app_redirect('/admin/');
+            app_redirect(app_url('/admin/'));
         }
         }
 
 
         $message = 'Login fehlgeschlagen. Bitte Zugangsdaten pruefen.';
         $message = 'Login fehlgeschlagen. Bitte Zugangsdaten pruefen.';
@@ -35,6 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             'timezone' => trim((string) ($_POST['timezone'] ?? 'Europe/Berlin')),
             'timezone' => trim((string) ($_POST['timezone'] ?? 'Europe/Berlin')),
             'dashboard_refresh_seconds' => max(5, (int) ($_POST['dashboard_refresh_seconds'] ?? 15)),
             'dashboard_refresh_seconds' => max(5, (int) ($_POST['dashboard_refresh_seconds'] ?? 15)),
             'default_from_email' => trim((string) ($_POST['default_from_email'] ?? 'monitor@example.local')),
             'default_from_email' => trim((string) ($_POST['default_from_email'] ?? 'monitor@example.local')),
+            'base_path' => app_normalize_base_path((string) ($_POST['base_path'] ?? '')),
         ];
         ];
         $config['api'] = [
         $config['api'] = [
             'bearer_token' => trim((string) ($_POST['bearer_token'] ?? 'change-me-token')),
             'bearer_token' => trim((string) ($_POST['bearer_token'] ?? 'change-me-token')),
@@ -75,7 +76,7 @@ function renderLogin(?string $message, string $messageType): void
         <meta charset="UTF-8">
         <meta charset="UTF-8">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>Adminpanel Login</title>
         <title>Adminpanel Login</title>
-        <link rel="stylesheet" href="/styles.css">
+        <link rel="stylesheet" href="<?= htmlspecialchars(app_url('/styles.css'), ENT_QUOTES) ?>">
     </head>
     </head>
     <body>
     <body>
         <main class="auth-page">
         <main class="auth-page">
@@ -97,7 +98,7 @@ function renderLogin(?string $message, string $messageType): void
                         <input type="password" name="password" required>
                         <input type="password" name="password" required>
                     </label>
                     </label>
                     <button class="button button--primary" type="submit">Einloggen</button>
                     <button class="button button--primary" type="submit">Einloggen</button>
-                    <a class="button button--ghost" href="/">Zurueck zum Dashboard</a>
+                    <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/'), ENT_QUOTES) ?>">Zurueck zum Dashboard</a>
                 </form>
                 </form>
             </section>
             </section>
         </main>
         </main>
@@ -115,7 +116,7 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
         <meta charset="UTF-8">
         <meta charset="UTF-8">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>Adminpanel</title>
         <title>Adminpanel</title>
-        <link rel="stylesheet" href="/styles.css">
+        <link rel="stylesheet" href="<?= htmlspecialchars(app_url('/styles.css'), ENT_QUOTES) ?>">
     </head>
     </head>
     <body>
     <body>
         <main class="admin-page">
         <main class="admin-page">
@@ -127,8 +128,8 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
                         <p>Hier werden API-Token, Zugangsdaten, Faecher und Alarmwege direkt in der JSON-Config gepflegt.</p>
                         <p>Hier werden API-Token, Zugangsdaten, Faecher und Alarmwege direkt in der JSON-Config gepflegt.</p>
                     </div>
                     </div>
                     <div class="inline-actions">
                     <div class="inline-actions">
-                        <a class="button button--ghost" href="/">Dashboard</a>
-                        <a class="button button--secondary" href="/admin/logout.php">Logout</a>
+                        <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/'), ENT_QUOTES) ?>">Dashboard</a>
+                        <a class="button button--secondary" href="<?= htmlspecialchars(app_url('/admin/logout.php'), ENT_QUOTES) ?>">Logout</a>
                     </div>
                     </div>
                 </div>
                 </div>
 
 
@@ -158,7 +159,12 @@ function renderAdmin(array $config, ?string $message, string $messageType): void
                                 Absender fuer Email
                                 Absender fuer Email
                                 <input type="email" name="default_from_email" value="<?= htmlspecialchars((string) ($config['app']['default_from_email'] ?? ''), ENT_QUOTES) ?>">
                                 <input type="email" name="default_from_email" value="<?= htmlspecialchars((string) ($config['app']['default_from_email'] ?? ''), ENT_QUOTES) ?>">
                             </label>
                             </label>
+                            <label>
+                                Basis-Pfad
+                                <input type="text" name="base_path" value="<?= htmlspecialchars((string) ($config['app']['base_path'] ?? ''), ENT_QUOTES) ?>" placeholder="/auswertung">
+                            </label>
                         </div>
                         </div>
+                        <p class="field-help">Leer lassen fuer den Domain-Root. Fuer Unterordner z. B. <code>/auswertung</code> eintragen.</p>
                     </section>
                     </section>
 
 
                     <section class="admin-grid">
                     <section class="admin-grid">

+ 1 - 1
public/admin/logout.php

@@ -5,4 +5,4 @@ declare(strict_types=1);
 require_once dirname(__DIR__, 2) . '/src/bootstrap.php';
 require_once dirname(__DIR__, 2) . '/src/bootstrap.php';
 
 
 app_admin_auth()->logout();
 app_admin_auth()->logout();
-app_redirect('/admin/');
+app_redirect(app_url('/admin/'));

+ 4 - 1
public/app.js

@@ -3,11 +3,14 @@ const dataNode = document.getElementById('initial-status');
 if (dataNode) {
 if (dataNode) {
   let currentStatus = JSON.parse(dataNode.textContent || '{}');
   let currentStatus = JSON.parse(dataNode.textContent || '{}');
   let activeMachine = 'all';
   let activeMachine = 'all';
+  const basePath = dataNode.dataset.basePath || '';
   const gridNode = document.getElementById('machine-grid');
   const gridNode = document.getElementById('machine-grid');
   const filterNode = document.getElementById('machine-filter');
   const filterNode = document.getElementById('machine-filter');
   const alertNode = document.getElementById('alert-list');
   const alertNode = document.getElementById('alert-list');
   const generatedAtNode = document.getElementById('generated-at');
   const generatedAtNode = document.getElementById('generated-at');
 
 
+  const appUrl = (path) => `${basePath}${path}`;
+
   const escapeHtml = (value) =>
   const escapeHtml = (value) =>
     String(value)
     String(value)
       .replaceAll('&', '&amp;')
       .replaceAll('&', '&amp;')
@@ -176,7 +179,7 @@ if (dataNode) {
 
 
   const refresh = async () => {
   const refresh = async () => {
     try {
     try {
-      const response = await fetch('/api/v1/status.php', { cache: 'no-store' });
+      const response = await fetch(appUrl('/api/v1/status.php'), { cache: 'no-store' });
       if (!response.ok) {
       if (!response.ok) {
         return;
         return;
       }
       }

+ 9 - 5
public/index.php

@@ -13,7 +13,7 @@ $appName = $status['app']['name'] ?? 'Getraenkeautomat Monitor';
     <meta charset="UTF-8">
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title><?= htmlspecialchars($appName, ENT_QUOTES) ?></title>
     <title><?= htmlspecialchars($appName, ENT_QUOTES) ?></title>
-    <link rel="stylesheet" href="/styles.css">
+    <link rel="stylesheet" href="<?= htmlspecialchars(app_url('/styles.css'), ENT_QUOTES) ?>">
 </head>
 </head>
 <body>
 <body>
     <div class="page-shell">
     <div class="page-shell">
@@ -40,8 +40,8 @@ $appName = $status['app']['name'] ?? 'Getraenkeautomat Monitor';
                 </div>
                 </div>
             </div>
             </div>
             <div class="hero__actions">
             <div class="hero__actions">
-                <a class="button button--primary" href="/admin/">Adminpanel</a>
-                <a class="button button--ghost" href="/api/v1/status.php" target="_blank" rel="noreferrer">Status JSON</a>
+                <a class="button button--primary" href="<?= htmlspecialchars(app_url('/admin/'), ENT_QUOTES) ?>">Adminpanel</a>
+                <a class="button button--ghost" href="<?= htmlspecialchars(app_url('/api/v1/status.php'), ENT_QUOTES) ?>" target="_blank" rel="noreferrer">Status JSON</a>
             </div>
             </div>
         </header>
         </header>
 
 
@@ -77,7 +77,11 @@ $appName = $status['app']['name'] ?? 'Getraenkeautomat Monitor';
         </main>
         </main>
     </div>
     </div>
 
 
-    <script id="initial-status" type="application/json"><?= json_encode($status, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?></script>
-    <script src="/app.js" defer></script>
+    <script
+        id="initial-status"
+        type="application/json"
+        data-base-path="<?= htmlspecialchars(app_base_path(), ENT_QUOTES) ?>"
+    ><?= json_encode($status, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?></script>
+    <script src="<?= htmlspecialchars(app_url('/app.js'), ENT_QUOTES) ?>" defer></script>
 </body>
 </body>
 </html>
 </html>

+ 1 - 1
public/openapi.yaml

@@ -13,7 +13,7 @@ info:
     The readings endpoint uses Bearer token authentication. The status endpoint is
     The readings endpoint uses Bearer token authentication. The status endpoint is
     intentionally public so the dashboard can poll it without a login.
     intentionally public so the dashboard can poll it without a login.
 servers:
 servers:
-  - url: /
+  - url: ./
     description: Same-origin deployment
     description: Same-origin deployment
 tags:
 tags:
   - name: Readings
   - name: Readings

+ 1 - 0
src/ConfigRepository.php

@@ -16,6 +16,7 @@ final class ConfigRepository
                 'timezone' => 'Europe/Berlin',
                 'timezone' => 'Europe/Berlin',
                 'dashboard_refresh_seconds' => 15,
                 'dashboard_refresh_seconds' => 15,
                 'default_from_email' => 'monitor@example.local',
                 'default_from_email' => 'monitor@example.local',
+                'base_path' => '',
             ],
             ],
             'api' => [
             'api' => [
                 'bearer_token' => 'change-me-token',
                 'bearer_token' => 'change-me-token',

+ 36 - 0
src/bootstrap.php

@@ -94,6 +94,42 @@ function app_redirect(string $location): never
     exit;
     exit;
 }
 }
 
 
+function app_normalize_base_path(?string $basePath): string
+{
+    $normalized = trim((string) $basePath);
+    if ($normalized === '' || $normalized === '/') {
+        return '';
+    }
+
+    $normalized = '/' . trim($normalized, '/');
+
+    return $normalized === '/' ? '' : $normalized;
+}
+
+function app_base_path(): string
+{
+    static $basePath = null;
+
+    if ($basePath === null) {
+        $config = app_config_repository()->getConfig();
+        $basePath = app_normalize_base_path((string) ($config['app']['base_path'] ?? ''));
+    }
+
+    return $basePath;
+}
+
+function app_url(string $path = '/'): string
+{
+    $basePath = app_base_path();
+    $normalizedPath = '/' . ltrim($path, '/');
+
+    if ($normalizedPath === '/') {
+        return $basePath === '' ? '/' : $basePath . '/';
+    }
+
+    return $basePath . $normalizedPath;
+}
+
 function app_read_json_body(): array
 function app_read_json_body(): array
 {
 {
     $raw = file_get_contents('php://input');
     $raw = file_get_contents('php://input');