Quellcode durchsuchen

added access controls

Bernn vor 2 Monaten
Ursprung
Commit
8de6828305
7 geänderte Dateien mit 233 neuen und 6 gelöschten Zeilen
  1. 10 0
      _private/config.php
  2. 7 0
      api.php
  3. 8 0
      auth.php
  4. 3 0
      index.php
  5. 75 0
      login.php
  6. 6 0
      logout.php
  7. 124 6
      upload.php

+ 10 - 0
_private/config.php

@@ -0,0 +1,10 @@
+<?php
+// Access codes for P&L viewer.
+// Managed via http://rktbds/upload.php — do not edit by hand.
+return array (
+  0 =>
+  array (
+    'label' => 'default',
+    'hash' => '$2y$10$Z4n1/aOBfta4VKQdku6wS.PjDQJGLH8Pj.WXK0UQ0/I0tqQzgq852',
+  ),
+);

+ 7 - 0
api.php

@@ -1,4 +1,11 @@
 <?php
+if (session_status() === PHP_SESSION_NONE) session_start();
+if (empty($_SESSION['pl_authed'])) {
+    http_response_code(401);
+    header('Content-Type: application/json; charset=utf-8');
+    echo json_encode(['error' => 'Unauthorized']);
+    exit;
+}
 header('Content-Type: application/json; charset=utf-8');
 header('Cache-Control: no-store');
 

+ 8 - 0
auth.php

@@ -0,0 +1,8 @@
+<?php
+// Shared session auth guard — include at the top of every protected HTML page.
+if (session_status() === PHP_SESSION_NONE) session_start();
+if (empty($_SESSION['pl_authed'])) {
+    $next = $_SERVER['REQUEST_URI'] ?? '/';
+    header('Location: /login.php?next=' . urlencode($next));
+    exit;
+}

+ 3 - 0
index.php

@@ -1,4 +1,5 @@
 <?php
+require __DIR__ . '/auth.php';
 $dbPath = __DIR__ . '/_private/pl.db';
 $hasData = file_exists($dbPath) && filesize($dbPath) > 0;
 ?>
@@ -192,6 +193,8 @@ tr.r-total td.neg{color:#fca5a5!important}
         <span style="color:#cbd5e1;margin:0 4px">|</span>
         <input type="checkbox" id="totals-only" onchange="onTotalsOnly()">
         <label for="totals-only">Totals Only</label>
+        <span style="color:#cbd5e1;margin:0 8px">|</span>
+        <a href="logout.php" style="font-size:12px;color:#94a3b8;text-decoration:none">Sign out</a>
       </div>
     </div>
 

+ 75 - 0
login.php

@@ -0,0 +1,75 @@
+<?php
+if (session_status() === PHP_SESSION_NONE) session_start();
+
+if (!empty($_SESSION['pl_authed'])) {
+    header('Location: /index.php');
+    exit;
+}
+
+$error = '';
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $pw    = $_POST['password'] ?? '';
+    $codes = file_exists(__DIR__ . '/_private/config.php')
+           ? (require __DIR__ . '/_private/config.php')
+           : [];
+    $ok = false;
+    foreach ($codes as $code) {
+        if (password_verify($pw, $code['hash'])) { $ok = true; break; }
+    }
+    if ($ok) {
+        session_regenerate_id(true);
+        $_SESSION['pl_authed'] = true;
+        $next = $_GET['next'] ?? '/index.php';
+        // Prevent open redirect — only allow relative paths on this host
+        if (!preg_match('/^\/[a-zA-Z0-9\/_\-\.]*$/', $next)) $next = '/index.php';
+        header('Location: ' . $next);
+        exit;
+    }
+    usleep(600000); // throttle brute-force
+    $error = 'Incorrect password. Please try again.';
+}
+?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>Sign In — ICG Magnetics</title>
+<style>
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;display:flex;align-items:center;justify-content:center}
+.card{background:#1e293b;border-radius:12px;padding:40px 44px;width:100%;max-width:380px;box-shadow:0 20px 60px rgba(0,0,0,.5)}
+.logo{font-size:13px;font-weight:600;color:#64748b;letter-spacing:.8px;text-transform:uppercase;margin-bottom:6px}
+h1{font-size:22px;font-weight:700;color:#f8fafc;margin-bottom:28px}
+label{display:block;font-size:12px;font-weight:600;color:#94a3b8;margin-bottom:6px;letter-spacing:.3px}
+input[type=password]{
+  display:block;width:100%;padding:11px 14px;
+  background:#0f172a;border:1px solid #334155;border-radius:7px;
+  color:#f1f5f9;font-size:15px;outline:none;
+  transition:border-color .15s
+}
+input[type=password]:focus{border-color:#3b82f6}
+.err{margin-top:14px;padding:10px 14px;background:#450a0a;border:1px solid #7f1d1d;border-radius:6px;color:#fca5a5;font-size:13px}
+button{
+  margin-top:22px;width:100%;padding:12px;
+  background:#3b82f6;border:none;border-radius:7px;
+  color:white;font-size:15px;font-weight:600;cursor:pointer;
+  transition:background .15s
+}
+button:hover{background:#2563eb}
+</style>
+</head>
+<body>
+<div class="card">
+  <div class="logo">ICG Magnetics</div>
+  <h1>Profit &amp; Loss</h1>
+  <form method="post" autocomplete="off">
+    <label for="pw">Access Code</label>
+    <input type="password" id="pw" name="password" autofocus placeholder="Enter access code">
+    <?php if ($error): ?>
+    <div class="err"><?= htmlspecialchars($error) ?></div>
+    <?php endif; ?>
+    <button type="submit">Sign In</button>
+  </form>
+</div>
+</body>
+</html>

+ 6 - 0
logout.php

@@ -0,0 +1,6 @@
+<?php
+if (session_status() === PHP_SESSION_NONE) session_start();
+$_SESSION = [];
+session_destroy();
+header('Location: /login.php');
+exit;

+ 124 - 6
upload.php

@@ -1,4 +1,5 @@
 <?php
+require __DIR__ . '/auth.php';
 $dbPath = __DIR__ . '/_private/pl.db';
 $message = '';
 $messageType = '';
@@ -177,19 +178,69 @@ function importCSV(string $filepath, string $dbPath): array {
     ];
 }
 
+$configPath = __DIR__ . '/_private/config.php';
+
+function loadCodes(string $path): array {
+    return file_exists($path) ? (require $path) : [];
+}
+
+function saveCodes(string $path, array $codes): void {
+    $export = "<?php\n// Access codes for P&L viewer.\n// Managed via http://rktbds/upload.php — do not edit by hand.\nreturn " . var_export($codes, true) . ";\n";
+    file_put_contents($path, $export);
+}
+
+$accessMsg  = '';
+$accessType = '';
+
 // Handle POST
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
     if (isset($_POST['import_existing'])) {
         $csvPath = __DIR__ . '/May2026PL.csv';
         $result  = importCSV($csvPath, $dbPath);
+        $message     = $result['msg'];
+        $messageType = $result['ok'] ? 'success' : 'error';
+
     } elseif (isset($_FILES['csvfile']) && $_FILES['csvfile']['error'] === UPLOAD_ERR_OK) {
         $result = importCSV($_FILES['csvfile']['tmp_name'], $dbPath);
+        $message     = $result['msg'];
+        $messageType = $result['ok'] ? 'success' : 'error';
+
+    } elseif (isset($_POST['add_code'])) {
+        $label = trim($_POST['code_label'] ?? '');
+        $pw    = $_POST['code_pw'] ?? '';
+        $pw2   = $_POST['code_pw2'] ?? '';
+        if ($label === '') {
+            $accessMsg = 'Label is required.'; $accessType = 'error';
+        } elseif (strlen($pw) < 8) {
+            $accessMsg = 'Access code must be at least 8 characters.'; $accessType = 'error';
+        } elseif ($pw !== $pw2) {
+            $accessMsg = 'Access codes do not match.'; $accessType = 'error';
+        } else {
+            $codes   = loadCodes($configPath);
+            $codes[] = ['label' => $label, 'hash' => password_hash($pw, PASSWORD_BCRYPT)];
+            saveCodes($configPath, $codes);
+            $accessMsg = "Access code for \"" . htmlspecialchars($label) . "\" added.";
+            $accessType = 'success';
+        }
+
+    } elseif (isset($_POST['remove_code'])) {
+        $idx   = (int)$_POST['remove_code'];
+        $codes = loadCodes($configPath);
+        if (count($codes) <= 1) {
+            $accessMsg = 'Cannot remove the last access code.'; $accessType = 'error';
+        } elseif (isset($codes[$idx])) {
+            $label = $codes[$idx]['label'];
+            array_splice($codes, $idx, 1);
+            saveCodes($configPath, $codes);
+            $accessMsg = "Access code for \"" . htmlspecialchars($label) . "\" removed.";
+            $accessType = 'success';
+        }
+
     } else {
         $uploadErr = $_FILES['csvfile']['error'] ?? -1;
-        $result    = ['ok'=>false,'msg'=>'Upload error (code '.$uploadErr.'). Check php.ini upload_max_filesize.'];
+        $message     = 'Upload error (code '.$uploadErr.'). Check php.ini upload_max_filesize.';
+        $messageType = 'error';
     }
-    $message     = $result['msg'];
-    $messageType = $result['ok'] ? 'success' : 'error';
 }
 
 $hasExistingCsv = file_exists(__DIR__ . '/May2026PL.csv');
@@ -273,9 +324,76 @@ input[type=file]:hover{border-color:#94a3b8}
     </form>
   </div>
 
-  <?php if ($hasDb && !$message): ?>
-  <a href="index.php" class="back">← Back to P&amp;L Viewer</a>
-  <?php endif; ?>
+  <!-- ── Access Codes ── -->
+  <div class="section">
+    <h2>Access Codes</h2>
+
+    <?php if ($accessMsg): ?>
+    <div class="alert <?= $accessType ?>" style="margin-bottom:14px"><?= $accessMsg ?></div>
+    <?php endif; ?>
+
+    <?php
+    $codes = loadCodes($configPath);
+    if ($codes):
+    ?>
+    <table style="width:100%;border-collapse:collapse;margin-bottom:14px;font-size:13px">
+      <thead>
+        <tr style="border-bottom:1px solid #e2e8f0">
+          <th style="text-align:left;padding:4px 8px;color:#64748b;font-weight:600">#</th>
+          <th style="text-align:left;padding:4px 8px;color:#64748b;font-weight:600">Label</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <?php foreach ($codes as $i => $code): ?>
+        <tr style="border-bottom:1px solid #f1f5f9">
+          <td style="padding:7px 8px;color:#94a3b8"><?= $i + 1 ?></td>
+          <td style="padding:7px 8px;font-weight:500"><?= htmlspecialchars($code['label']) ?></td>
+          <td style="padding:7px 8px;text-align:right">
+            <form method="post" style="display:inline" onsubmit="return confirm('Remove access for \'<?= htmlspecialchars(addslashes($code['label'])) ?>\'?')">
+              <button type="submit" name="remove_code" value="<?= $i ?>"
+                      style="background:none;border:none;color:#ef4444;font-size:12px;cursor:pointer;font-weight:600"
+                      <?= count($codes) <= 1 ? 'disabled title="Cannot remove last code"' : '' ?>>
+                Remove
+              </button>
+            </form>
+          </td>
+        </tr>
+        <?php endforeach; ?>
+      </tbody>
+    </table>
+    <?php endif; ?>
+
+    <form method="post" style="display:grid;gap:8px">
+      <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
+        <div>
+          <label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Label (e.g. name)</label>
+          <input type="text" name="code_label" placeholder="Alice"
+                 style="width:100%;padding:7px 10px;border:1px solid #cbd5e1;border-radius:5px;font-size:13px">
+        </div>
+        <div>
+          <label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Access Code</label>
+          <input type="password" name="code_pw" placeholder="min 8 characters"
+                 style="width:100%;padding:7px 10px;border:1px solid #cbd5e1;border-radius:5px;font-size:13px">
+        </div>
+      </div>
+      <div>
+        <label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Confirm Access Code</label>
+        <input type="password" name="code_pw2" placeholder="repeat access code"
+               style="width:100%;padding:7px 10px;border:1px solid #cbd5e1;border-radius:5px;font-size:13px">
+      </div>
+      <button type="submit" name="add_code" class="btn btn-primary" style="justify-self:start;margin-top:4px">Add Access Code</button>
+    </form>
+  </div>
+
+  <div style="display:flex;justify-content:space-between;align-items:center;margin-top:4px">
+    <?php if ($hasDb): ?>
+    <a href="index.php" class="back">← Back to P&amp;L Viewer</a>
+    <?php else: ?>
+    <span></span>
+    <?php endif; ?>
+    <a href="logout.php" style="font-size:13px;color:#94a3b8;text-decoration:none">Sign out</a>
+  </div>
 </div>
 </body>
 </html>