()
| 143 | } |
| 144 | |
| 145 | public function addUser() |
| 146 | { |
| 147 | self::jsonHeaders(); |
| 148 | self::requireMethod(['POST']); |
| 149 | |
| 150 | // Initialize CSRF token if missing (useful for initial page load) |
| 151 | if (empty($_SESSION['csrf_token'])) { |
| 152 | $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); |
| 153 | } |
| 154 | |
| 155 | // Setup mode detection (first-run bootstrap) |
| 156 | $isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1'); |
| 157 | $setupMode = false; |
| 158 | |
| 159 | if ($isSetup && UserModel::isInitialSetupAllowed()) { |
| 160 | $setupMode = true; |
| 161 | } else { |
| 162 | // Not setup: enforce CSRF + admin auth |
| 163 | $h = self::headersLower(); |
| 164 | $receivedToken = trim($h['x-csrf-token'] ?? ''); |
| 165 | |
| 166 | // Soft-fail CSRF: on mismatch, regenerate and return new token |
| 167 | if ($receivedToken !== $_SESSION['csrf_token']) { |
| 168 | $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); |
| 169 | header('X-CSRF-Token: ' . $_SESSION['csrf_token']); |
| 170 | echo json_encode([ |
| 171 | 'csrf_expired' => true, |
| 172 | 'csrf_token' => $_SESSION['csrf_token'], |
| 173 | ]); |
| 174 | exit; |
| 175 | } |
| 176 | |
| 177 | self::requireAdmin(); |
| 178 | } |
| 179 | |
| 180 | $data = self::readJson(); |
| 181 | $newUsername = trim($data['username'] ?? ''); |
| 182 | $newPassword = trim($data['password'] ?? ''); |
| 183 | |
| 184 | $isAdmin = $setupMode ? '1' : (!empty($data['isAdmin']) ? '1' : '0'); |
| 185 | |
| 186 | // Helper for validation errors (new + legacy shape) |
| 187 | $fail = function (string $message, int $status = 422) { |
| 188 | http_response_code($status); |
| 189 | echo json_encode([ |
| 190 | 'ok' => false, |
| 191 | 'error' => $message, |
| 192 | // legacy-friendly fields: |
| 193 | 'success' => false, |
| 194 | 'message' => $message, |
| 195 | ]); |
| 196 | exit; |
| 197 | }; |
| 198 | |
| 199 | if ($newUsername === '' || $newPassword === '') { |
| 200 | $fail('Username and password required'); |
| 201 | } |
| 202 |
no test coverage detected