Files
nat-as-server/api/users.php

275 lines
11 KiB
PHP

<?php
require_once __DIR__ . '/../common.php';
require_once __DIR__ . '/../db_init.php';
header('Content-Type: application/json; charset=UTF-8');
$tokenRec = require_valid_token_web();
$callerRole = $tokenRec['role'];
$callerEntityID = $tokenRec['entityID'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
// ── GET: list users (+ supervisors for admin) ─────────────────────────────
if ($method === 'GET') {
try {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
if ($callerRole === 'admin') {
$users = $pdo->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);
} else {
// Supervisor: only their coaches
$svNameStmt = $pdo->prepare("SELECT username FROM supervisor WHERE supervisorID = :svid");
$svNameStmt->execute([':svid' => $callerEntityID]);
$svName = $svNameStmt->fetchColumn() ?: '';
$stmt = $pdo->prepare(
"SELECT u.userID, u.username, u.role, u.entityID, u.mustChangePassword, u.createdAt,
'' AS location, c.supervisorID, :svname AS supervisorUsername
FROM users u
JOIN coach c ON c.coachID = u.entityID AND u.role = 'coach'
WHERE c.supervisorID = :svid
ORDER BY u.username"
);
$stmt->execute([':svid' => $callerEntityID, ':svname' => $svName]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
$supervisors = [];
}
qdb_discard($tmpDb, $lockFp);
echo json_encode([
"success" => true,
"users" => $users,
"supervisors" => $supervisors,
"callerRole" => $callerRole,
]);
} 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;
}
// ── POST: create a user ───────────────────────────────────────────────────
if ($method === 'POST') {
$in = json_decode(file_get_contents('php://input'), 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 ($callerRole === 'supervisor' && $role !== 'coach') {
http_response_code(403);
echo json_encode(["error" => "Supervisors may only create coaches"]);
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;
}
// Supervisors always create coaches under themselves
if ($callerRole === 'supervisor') {
$supervisorID = $callerEntityID;
}
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') {
$svCheck = $pdo->prepare("SELECT supervisorID FROM supervisor WHERE supervisorID = :sid");
$svCheck->execute([':sid' => $supervisorID]);
if (!$svCheck->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(400);
echo json_encode(["error" => "Supervisor not found"]);
exit;
}
if ($callerRole === 'supervisor' && $supervisorID !== $callerEntityID) {
qdb_discard($tmpDb, $lockFp);
http_response_code(403);
echo json_encode(["error" => "Cannot create coaches for another supervisor"]);
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();
qdb_save($tmpDb, $lockFp);
// Fetch supervisor name for response
$svUsername = null;
if ($role === 'coach') {
[$pdo2, $tmp2, $lk2] = qdb_open(false);
$svr = $pdo2->prepare("SELECT username FROM supervisor WHERE supervisorID = :sid");
$svr->execute([':sid' => $supervisorID]);
$svUsername = $svr->fetchColumn() ?: null;
qdb_discard($tmp2, $lk2);
}
echo json_encode([
"success" => true,
"userID" => $userID,
"entityID" => $entityID,
"username" => $username,
"role" => $role,
"location" => $location,
"supervisorID" => $role === 'coach' ? $supervisorID : null,
"supervisorUsername" => $svUsername,
"mustChangePassword" => $mustChange,
"createdAt" => $now,
]);
} 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;
}
// ── DELETE: remove a user ─────────────────────────────────────────────────
if ($method === 'DELETE') {
$in = json_decode(file_get_contents('php://input'), true) ?: [];
$targetUserID = trim($in['userID'] ?? '');
if ($targetUserID === '') {
http_response_code(400);
echo json_encode(["error" => "userID is required"]);
exit;
}
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;
}
if ($callerRole === 'supervisor') {
if ($target['role'] !== 'coach') {
qdb_discard($tmpDb, $lockFp);
http_response_code(403);
echo json_encode(["error" => "Supervisors can only delete coaches"]);
exit;
}
$chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :svid");
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp);
http_response_code(403);
echo json_encode(["error" => "Coach is not under your supervision"]);
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();
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"]);