GitGram — setup.php — GitGram
GitGram / main / v2.00 / setup.php19,438 B↓ Raw
<?php
// ─────────────────────────────────────────────────────────────────────────────
//  GitGram setup.php — first-run account manager
//  DELETE or rename this file after setup is complete.
// ─────────────────────────────────────────────────────────────────────────────

define('DATA_PATH', __DIR__ . '/data');
define('LOCK_FILE', DATA_PATH . '/setup.lock');
define('USERS_FILE', DATA_PATH . '/users.json');

$locked   = file_exists(LOCK_FILE);
$errors   = [];
$success  = [];
$action   = $_POST['action'] ?? '';

// ── Handle form submissions ───────────────────────────────────────────────────

if (!$locked && $_SERVER['REQUEST_METHOD'] === 'POST') {

    // Verify the setup key before doing anything
    $setup_key = trim($_POST['setup_key'] ?? '');
    $stored_key = trim(@file_get_contents(LOCK_FILE . '.key') ?: '');

    // First POST ever — no key file yet, so accept whatever key was typed and store it
    if (!file_exists(LOCK_FILE . '.key')) {
        if (strlen($setup_key) < 8) {
            $errors[] = 'Setup key must be at least 8 characters. Choose something memorable — you will need it for later changes.';
        } else {
            file_put_contents(LOCK_FILE . '.key', $setup_key);
            $stored_key = $setup_key;
        }
    }

    if (!$errors && $setup_key !== $stored_key) {
        $errors[] = 'Incorrect setup key.';
    }

    if (!$errors) {
        $users = json_decode(@file_get_contents(USERS_FILE), true) ?: [];

        if ($action === 'add_user') {
            $username = trim($_POST['username'] ?? '');
            $name     = trim($_POST['name']     ?? '');
            $password = $_POST['password']      ?? '';
            $confirm  = $_POST['confirm']       ?? '';
            $role     = in_array($_POST['role'] ?? '', ['admin', 'user']) ? $_POST['role'] : 'user';
            $groups   = array_values(array_filter(array_map('trim', explode(',', $_POST['groups'] ?? ''))));

            if (!preg_match('/^[a-z0-9_\-]{2,32}$/', $username))
                $errors[] = 'Username must be 2–32 lowercase letters, numbers, hyphens, or underscores.';
            if (!$name)
                $errors[] = 'Display name is required.';
            if (strlen($password) < 8)
                $errors[] = 'Password must be at least 8 characters.';
            if ($password !== $confirm)
                $errors[] = 'Passwords do not match.';

            if (!$errors) {
                $users[$username] = [
                    'name'          => $name,
                    'password_hash' => password_hash($password, PASSWORD_DEFAULT),
                    'role'          => $role,
                    'groups'        => $groups,
                ];
                file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
                $success[] = "User <strong>" . htmlspecialchars($username) . "</strong> saved.";
            }

        } elseif ($action === 'change_password') {
            $username = trim($_POST['username'] ?? '');
            $password = $_POST['password']      ?? '';
            $confirm  = $_POST['confirm']       ?? '';

            if (!isset($users[$username]))
                $errors[] = 'User not found.';
            if (strlen($password) < 8)
                $errors[] = 'Password must be at least 8 characters.';
            if ($password !== $confirm)
                $errors[] = 'Passwords do not match.';

            if (!$errors) {
                $users[$username]['password_hash'] = password_hash($password, PASSWORD_DEFAULT);
                file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
                $success[] = "Password updated for <strong>" . htmlspecialchars($username) . "</strong>.";
            }

        } elseif ($action === 'delete_user') {
            $username = trim($_POST['username'] ?? '');
            $users_left = array_filter($users, fn($u) => $u['role'] === 'admin');
            if (count($users_left) <= 1 && isset($users[$username]) && $users[$username]['role'] === 'admin') {
                $errors[] = 'Cannot delete the last admin account.';
            } elseif (!isset($users[$username])) {
                $errors[] = 'User not found.';
            } else {
                unset($users[$username]);
                file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
                $success[] = "User <strong>" . htmlspecialchars($username) . "</strong> deleted.";
            }

        } elseif ($action === 'lock') {
            file_put_contents(LOCK_FILE, date('Y-m-d H:i:s'));
            $locked = true;
        }
    }
}

// ── Load current users for display ───────────────────────────────────────────
$users = json_decode(@file_get_contents(USERS_FILE), true) ?: [];
$has_real_admin = false;
foreach ($users as $u) {
    if ($u['role'] === 'admin' && !str_starts_with($u['password_hash'], '$2y$12$placeholder')) {
        $has_real_admin = true;
        break;
    }
}
$key_set = file_exists(LOCK_FILE . '.key');

// ── HTML ──────────────────────────────────────────────────────────────────────
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitGram Setup</title>
<style>
:root {
  --desktop:   #008080;
  --win-bg:    #c0c0c0;
  --title-bg:  #000080;
  --title-txt: #ffffff;
  --hi:        #ffffff;
  --sh:        #808080;
  --dk:        #404040;
  --warn-bg:   #fffbe6;
  --warn-bdr:  #e0c000;
  --err-bg:    #fff0f0;
  --err-bdr:   #cc0000;
  --ok-bg:     #f0fff0;
  --ok-bdr:    #008000;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  background: var(--desktop);
  font-family: "Arial", "Helvetica", sans-serif;
  font-size: 13px;
  padding: 14px;
  min-height: 100vh;
}
.window {
  max-width: 700px; margin: 0 auto;
  background: var(--win-bg);
  border-top:   2px solid var(--hi);
  border-left:  2px solid var(--hi);
  border-right: 2px solid var(--dk);
  border-bottom:2px solid var(--dk);
}
.titlebar {
  background: var(--title-bg); color: var(--title-txt);
  display: flex; align-items: center; height: 22px; padding: 0 3px; gap: 2px;
  user-select: none;
}
.titlebar-sysmenu {
  width: 18px; height: 14px; background: var(--win-bg); color: #000;
  border-top:1px solid var(--hi); border-left:1px solid var(--hi);
  border-right:1px solid var(--dk); border-bottom:1px solid var(--dk);
  display:flex; align-items:center; justify-content:center; font-size:9px;
}
.titlebar-title { flex:1; text-align:center; font-size:12px; font-weight:bold; }
.titlebar-btn {
  width:16px; height:14px; background:var(--win-bg); color:#000;
  border-top:1px solid var(--hi); border-left:1px solid var(--hi);
  border-right:1px solid var(--dk); border-bottom:1px solid var(--dk);
  display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:bold;
}
.win-body {
  border-top:1px solid var(--sh); border-left:1px solid var(--sh);
  border-right:1px solid var(--hi); border-bottom:1px solid var(--hi);
  margin: 4px; background: #fff;
}
.toolbar {
  background: #b0b0b0;
  border-bottom:1px solid var(--sh); border-top:1px solid var(--hi);
  padding: 4px 10px; font-size:12px; font-weight:bold;
}
.pad { padding: 14px 16px; }
.section { margin-bottom: 22px; }
.section-title {
  font-weight:bold; font-size:12px; border-bottom:1px solid #aaa;
  padding-bottom:3px; margin-bottom:10px;
}
label { display:block; font-weight:bold; margin-bottom:3px; font-size:12px; }
input[type=text], input[type=password], input[type=email], select {
  width:100%; padding:4px 6px; border:2px inset #aaa;
  font-size:13px; font-family:inherit; background:#fff;
  margin-bottom:10px;
}
input[type=text]:focus, input[type=password]:focus, select:focus {
  outline: 2px solid var(--title-bg);
}
.row { display:flex; gap:12px; }
.row > div { flex:1; }
.btn {
  padding: 4px 16px; font-size:12px; font-family:inherit; cursor:pointer;
  background: var(--win-bg);
  border-top:2px solid var(--hi); border-left:2px solid var(--hi);
  border-right:2px solid var(--dk); border-bottom:2px solid var(--dk);
}
.btn:active {
  border-top:2px solid var(--dk); border-left:2px solid var(--dk);
  border-right:2px solid var(--hi); border-bottom:2px solid var(--hi);
}
.btn-danger { background: #ffcccc; }
.btn-primary { background: #ccccff; font-weight:bold; }
.btn-lock { background: #ffeecc; font-weight:bold; }
.alert {
  padding: 8px 12px; margin-bottom: 12px; font-size: 12px; border-left: 4px solid;
}
.alert-error { background: var(--err-bg); border-color: var(--err-bdr); color: #800; }
.alert-ok    { background: var(--ok-bg);  border-color: var(--ok-bdr);  color: #040; }
.alert-warn  { background: var(--warn-bg);border-color: var(--warn-bdr);color: #640; }
.user-table { width:100%; border-collapse:collapse; font-size:12px; margin-bottom:10px; }
.user-table th { background:#e0e0e0; padding:4px 8px; text-align:left; border-bottom:2px solid #aaa; }
.user-table td { padding:4px 8px; border-bottom:1px solid #ddd; vertical-align:middle; }
.user-table tr:hover td { background: #eeeeff; }
.badge {
  display:inline-block; font-size:10px; padding:1px 6px;
  border-radius:2px; font-weight:bold;
}
.badge-admin { background:#000080; color:#fff; }
.badge-user  { background:#888; color:#fff; }
.hint { font-size:11px; color:#666; margin-top:-7px; margin-bottom:10px; }
.locked-msg { text-align:center; padding:30px 20px; }
.locked-msg h2 { font-size:18px; margin-bottom:10px; color:#000080; }
.locked-msg p { margin-bottom:8px; color:#444; }
.locked-msg code { background:#eee; padding:2px 6px; font-family:monospace; border:1px solid #ccc; }
hr { border:none; border-top:1px solid #bbb; margin:20px 0; }
.tab-bar { display:flex; gap:0; border-bottom:2px solid #aaa; margin-bottom:14px; }
.tab {
  padding:4px 14px; font-size:12px; cursor:pointer; background:#d4d4d4;
  border-top:1px solid var(--hi); border-left:1px solid var(--hi);
  border-right:1px solid var(--sh); border-bottom:none;
  margin-right:2px; margin-bottom:-2px;
}
.tab.active { background:#fff; border-bottom:2px solid #fff; font-weight:bold; }
.tab-panel { display:none; }
.tab-panel.active { display:block; }
</style>
</head>
<body>
<div class="window">

  <div class="titlebar">
    <div class="titlebar-sysmenu">▪</div>
    <div class="titlebar-title">GitGram — Account Setup</div>
    <div class="titlebar-btn">▼</div>
    <div class="titlebar-btn">▲</div>
    <div class="titlebar-btn">✕</div>
  </div>

  <div class="toolbar">Account Setup</div>

  <div class="win-body">
  <div class="pad">

<?php if ($locked): ?>

  <div class="locked-msg">
    <h2>&#128274; Setup is locked</h2>
    <p>Account setup has been completed and locked.</p>
    <p>For security, delete this file from your server:</p>
    <p><code>rm setup.php</code></p>
    <p>To make further changes, delete <code>data/setup.lock</code> and reload this page.</p>
  </div>

<?php else: ?>

  <?php if (!$has_real_admin): ?>
  <div class="alert alert-warn">
    &#9888; No admin account has a real password yet. Set one before using GitGram.
  </div>
  <?php endif; ?>

  <?php if (!$key_set): ?>
  <div class="alert alert-warn">
    &#128273; <strong>First run:</strong> Choose a setup key below. You will need it every time you use this page.
    It is <em>not</em> a git login — it just protects this setup tool.
  </div>
  <?php endif; ?>

  <?php foreach ($errors as $e): ?>
    <div class="alert alert-error">&#10060; <?= $e ?></div>
  <?php endforeach; ?>
  <?php foreach ($success as $s): ?>
    <div class="alert alert-ok">&#10003; <?= $s ?></div>
  <?php endforeach; ?>

  <!-- Current users -->
  <div class="section">
    <div class="section-title">Current Users</div>
    <?php if (empty($users)): ?>
      <p style="color:#888">No users yet.</p>
    <?php else: ?>
    <table class="user-table">
      <tr><th>Username</th><th>Display Name</th><th>Role</th><th>Groups</th></tr>
      <?php foreach ($users as $uname => $u): ?>
      <tr>
        <td><strong><?= htmlspecialchars($uname) ?></strong></td>
        <td><?= htmlspecialchars($u['name'] ?? '') ?></td>
        <td><span class="badge badge-<?= $u['role'] ?? 'user' ?>"><?= htmlspecialchars($u['role'] ?? 'user') ?></span></td>
        <td style="color:#666; font-size:11px"><?= htmlspecialchars(implode(', ', $u['groups'] ?? [])) ?></td>
      </tr>
      <?php endforeach; ?>
    </table>
    <?php endif; ?>
  </div>

  <!-- Tab UI -->
  <div class="tab-bar">
    <div class="tab active" onclick="switchTab('tab-add')">Add / Update User</div>
    <div class="tab" onclick="switchTab('tab-pw')">Change Password</div>
    <div class="tab" onclick="switchTab('tab-del')">Delete User</div>
    <div class="tab" onclick="switchTab('tab-lock')">Lock Setup</div>
  </div>

  <!-- ── Add / Update User ── -->
  <div class="tab-panel active" id="tab-add">
    <form method="post">
      <input type="hidden" name="action" value="add_user">
      <label>Setup Key</label>
      <input type="password" name="setup_key" autocomplete="off" placeholder="<?= $key_set ? 'Enter your setup key' : 'Choose a setup key (min 8 chars)' ?>">

      <div class="row">
        <div>
          <label>Username</label>
          <input type="text" name="username" placeholder="e.g. alice" autocomplete="off"
                 pattern="[a-z0-9_\-]{2,32}" title="Lowercase letters, numbers, hyphens, underscores">
          <p class="hint">Lowercase, 2–32 chars. Used for git push login.</p>
        </div>
        <div>
          <label>Display Name</label>
          <input type="text" name="name" placeholder="e.g. Alice Smith">
        </div>
      </div>

      <div class="row">
        <div>
          <label>Password</label>
          <input type="password" name="password" autocomplete="new-password" placeholder="Min 8 characters">
        </div>
        <div>
          <label>Confirm Password</label>
          <input type="password" name="confirm" autocomplete="new-password" placeholder="Repeat password">
        </div>
      </div>

      <div class="row">
        <div>
          <label>Role</label>
          <select name="role">
            <option value="user">user — normal access</option>
            <option value="admin">admin — full access to all repos</option>
          </select>
        </div>
        <div>
          <label>Groups</label>
          <input type="text" name="groups" placeholder="e.g. devs, leads">
          <p class="hint">Comma-separated. Used in repos.json access rules (@devs, etc.)</p>
        </div>
      </div>

      <button type="submit" class="btn btn-primary">Save User</button>
      <span style="margin-left:10px; font-size:11px; color:#666">
        Updating an existing username will overwrite it.
      </span>
    </form>
  </div>

  <!-- ── Change Password ── -->
  <div class="tab-panel" id="tab-pw">
    <form method="post">
      <input type="hidden" name="action" value="change_password">
      <label>Setup Key</label>
      <input type="password" name="setup_key" autocomplete="off" placeholder="Enter your setup key">

      <label>Username</label>
      <select name="username">
        <?php foreach ($users as $uname => $u): ?>
          <option value="<?= htmlspecialchars($uname) ?>"><?= htmlspecialchars($uname) ?> (<?= htmlspecialchars($u['name'] ?? '') ?>)</option>
        <?php endforeach; ?>
      </select>

      <div class="row">
        <div>
          <label>New Password</label>
          <input type="password" name="password" autocomplete="new-password" placeholder="Min 8 characters">
        </div>
        <div>
          <label>Confirm Password</label>
          <input type="password" name="confirm" autocomplete="new-password" placeholder="Repeat password">
        </div>
      </div>

      <button type="submit" class="btn btn-primary">Change Password</button>
    </form>
  </div>

  <!-- ── Delete User ── -->
  <div class="tab-panel" id="tab-del">
    <form method="post" onsubmit="return confirm('Delete this user? This cannot be undone.')">
      <input type="hidden" name="action" value="delete_user">
      <label>Setup Key</label>
      <input type="password" name="setup_key" autocomplete="off" placeholder="Enter your setup key">

      <label>User to Delete</label>
      <select name="username">
        <?php foreach ($users as $uname => $u): ?>
          <option value="<?= htmlspecialchars($uname) ?>"><?= htmlspecialchars($uname) ?> — <?= htmlspecialchars($u['name'] ?? '') ?> [<?= $u['role'] ?? 'user' ?>]</option>
        <?php endforeach; ?>
      </select>

      <button type="submit" class="btn btn-danger">Delete User</button>
    </form>
  </div>

  <!-- ── Lock Setup ── -->
  <div class="tab-panel" id="tab-lock">
    <div class="alert alert-warn" style="margin-bottom:14px">
      &#128274; Locking prevents anyone from accessing this setup page until you delete
      <code>data/setup.lock</code>. Do this when you are done configuring accounts.
    </div>
    <?php if (!$has_real_admin): ?>
    <div class="alert alert-error">
      &#9888; You have not set a real admin password yet. Lock anyway?
    </div>
    <?php endif; ?>
    <form method="post" onsubmit="return confirm('Lock setup? You will need to delete data/setup.lock to unlock.')">
      <input type="hidden" name="action" value="lock">
      <label>Setup Key</label>
      <input type="password" name="setup_key" autocomplete="off" placeholder="Enter your setup key" style="max-width:300px">
      <button type="submit" class="btn btn-lock">&#128274; Lock Setup &amp; Finish</button>
    </form>
    <hr>
    <p style="font-size:11px; color:#666">
      After locking, delete <code>setup.php</code> from your server for maximum security.
      The <code>data/setup.lock.key</code> file also contains your setup key in plaintext — delete it too once you no longer need this tool.
    </p>
  </div>

<?php endif; ?>

  </div><!-- pad -->
  </div><!-- win-body -->

  <div style="background:var(--win-bg); border-top:1px solid var(--sh); padding:2px 10px; font-size:11px; color:#555; display:flex; gap:8px;">
    <span style="border:1px inset #aaa; padding:1px 8px">GitGram Setup</span>
    <span style="border:1px inset #aaa; padding:1px 8px"><?= htmlspecialchars(USERS_FILE) ?></span>
  </div>

</div><!-- window -->

<script>
function switchTab(id) {
  document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  document.getElementById(id).classList.add('active');
  event.target.classList.add('active');
}
</script>
</body>
</html>
Ready
GitGram