<?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>🔒 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">
⚠ 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">
🔑 <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">❌ <?= $e ?></div>
<?php endforeach; ?>
<?php foreach ($success as $s): ?>
<div class="alert alert-ok">✓ <?= $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">
🔒 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">
⚠ 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">🔒 Lock Setup & 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>