query( "SELECT u.userID, u.username, u.role, u.entityID, u.mustChangePassword, u.createdAt, COALESCE(a.location, s.location, '') AS location, c.supervisorID, sv.username AS supervisorUsername FROM users u LEFT JOIN admin a ON a.adminID = u.entityID AND u.role = 'admin' LEFT JOIN supervisor s ON s.supervisorID = u.entityID AND u.role = 'supervisor' LEFT JOIN coach c ON c.coachID = u.entityID AND u.role = 'coach' LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID ORDER BY u.role, u.username" )->fetchAll(PDO::FETCH_ASSOC); $supervisors = $pdo->query( "SELECT supervisorID, username, location FROM supervisor ORDER BY username" )->fetchAll(PDO::FETCH_ASSOC); $pdo = null; qdb_discard($tmpDb, $lockFp); echo json_encode(["users" => $users, "supervisors" => $supervisors]); } catch (Throwable $e) { if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); http_response_code(500); echo json_encode(["error" => "Server error"]); } exit; } // ----------------------------------------------------------------------- // POST: create a user // ----------------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'POST') { $raw = file_get_contents("php://input"); $in = json_decode($raw, true) ?: []; $username = trim($in['username'] ?? ''); $password = (string)($in['password'] ?? ''); $role = strtolower(trim($in['role'] ?? '')); $location = trim($in['location'] ?? ''); $supervisorID = trim($in['supervisorID'] ?? ''); $mustChange = isset($in['mustChangePassword']) ? (int)(bool)$in['mustChangePassword'] : 1; if ($username === '' || $password === '' || $role === '') { http_response_code(400); echo json_encode(["error" => "username, password, and role are required"]); exit; } if (!in_array($role, ['admin', 'supervisor', 'coach'], true)) { http_response_code(400); echo json_encode(["error" => "role must be admin, supervisor, or coach"]); exit; } if (strlen($password) < 6) { http_response_code(400); echo json_encode(["error" => "Password must be at least 6 characters"]); exit; } if ($role === 'coach' && $supervisorID === '') { http_response_code(400); echo json_encode(["error" => "supervisorID is required for coaches"]); exit; } try { [$pdo, $tmpDb, $lockFp] = qdb_open(true); $chk = $pdo->prepare("SELECT userID FROM users WHERE username = :u"); $chk->execute([':u' => $username]); if ($chk->fetch()) { qdb_discard($tmpDb, $lockFp); http_response_code(409); echo json_encode(["error" => "Username already exists"]); exit; } if ($role === 'coach') { $sv = $pdo->prepare("SELECT supervisorID FROM supervisor WHERE supervisorID = :sid"); $sv->execute([':sid' => $supervisorID]); if (!$sv->fetch()) { qdb_discard($tmpDb, $lockFp); http_response_code(400); echo json_encode(["error" => "supervisorID not found"]); exit; } } $entityID = bin2hex(random_bytes(16)); $userID = bin2hex(random_bytes(16)); $hash = password_hash($password, PASSWORD_DEFAULT); $now = time(); $pdo->beginTransaction(); switch ($role) { case 'admin': $pdo->prepare("INSERT INTO admin (adminID, username, location) VALUES (:id, :u, :loc)") ->execute([':id' => $entityID, ':u' => $username, ':loc' => $location]); break; case 'supervisor': $pdo->prepare("INSERT INTO supervisor (supervisorID, username, location) VALUES (:id, :u, :loc)") ->execute([':id' => $entityID, ':u' => $username, ':loc' => $location]); break; case 'coach': $pdo->prepare("INSERT INTO coach (coachID, supervisorID, username) VALUES (:id, :sid, :u)") ->execute([':id' => $entityID, ':sid' => $supervisorID, ':u' => $username]); break; } $pdo->prepare( "INSERT INTO users (userID, username, passwordHash, role, entityID, mustChangePassword, createdAt) VALUES (:uid, :u, :hash, :role, :eid, :mcp, :now)" )->execute([ ':uid' => $userID, ':u' => $username, ':hash' => $hash, ':role' => $role, ':eid' => $entityID, ':mcp' => $mustChange, ':now' => $now, ]); $pdo->commit(); $pdo = null; qdb_save($tmpDb, $lockFp); echo json_encode([ "success" => true, "userID" => $userID, "entityID" => $entityID, "username" => $username, "role" => $role, ]); } catch (Throwable $e) { if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); http_response_code(500); echo json_encode(["error" => "Server error"]); } exit; } // ----------------------------------------------------------------------- // DELETE: remove a user (and their role entity row) // ----------------------------------------------------------------------- if ($_SERVER['REQUEST_METHOD'] === 'DELETE') { $raw = file_get_contents("php://input"); $in = json_decode($raw, true) ?: []; $targetUserID = trim($in['userID'] ?? ''); if ($targetUserID === '') { http_response_code(400); echo json_encode(["error" => "userID is required"]); exit; } // Prevent self-deletion if ($targetUserID === ($tokenRec['userID'] ?? '')) { http_response_code(400); echo json_encode(["error" => "Cannot delete your own account"]); exit; } try { [$pdo, $tmpDb, $lockFp] = qdb_open(true); $row = $pdo->prepare("SELECT role, entityID FROM users WHERE userID = :uid"); $row->execute([':uid' => $targetUserID]); $target = $row->fetch(PDO::FETCH_ASSOC); if (!$target) { qdb_discard($tmpDb, $lockFp); http_response_code(404); echo json_encode(["error" => "User not found"]); exit; } $pdo->beginTransaction(); $pdo->prepare("DELETE FROM users WHERE userID = :uid") ->execute([':uid' => $targetUserID]); switch ($target['role']) { case 'admin': $pdo->prepare("DELETE FROM admin WHERE adminID = :id") ->execute([':id' => $target['entityID']]); break; case 'supervisor': $pdo->prepare("DELETE FROM supervisor WHERE supervisorID = :id") ->execute([':id' => $target['entityID']]); break; case 'coach': $pdo->prepare("DELETE FROM coach WHERE coachID = :id") ->execute([':id' => $target['entityID']]); break; } $pdo->commit(); $pdo = null; qdb_save($tmpDb, $lockFp); echo json_encode(["success" => true]); } catch (Throwable $e) { if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); http_response_code(500); error_log($e->getMessage()); echo json_encode(["error" => "Server error"]); } exit; } http_response_code(405); echo json_encode(["error" => "Method not allowed"]);