428 lines
17 KiB
PHP
428 lines
17 KiB
PHP
<?php
|
|
|
|
$tokenRec = require_valid_token_web();
|
|
|
|
$callerRole = $tokenRec['role'];
|
|
$callerEntityID = $tokenRec['entityID'] ?? '';
|
|
|
|
switch ($method) {
|
|
|
|
case 'GET':
|
|
try {
|
|
[$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail();
|
|
|
|
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 {
|
|
$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);
|
|
json_success([
|
|
'users' => $users,
|
|
'supervisors' => $supervisors,
|
|
'callerRole' => $callerRole,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'users', null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
|
|
case 'POST':
|
|
$body = read_json_body();
|
|
$username = trim($body['username'] ?? '');
|
|
$password = (string)($body['password'] ?? '');
|
|
$role = strtolower(trim($body['role'] ?? ''));
|
|
$location = trim($body['location'] ?? '');
|
|
$supervisorID = trim($body['supervisorID'] ?? '');
|
|
$mustChange = isset($body['mustChangePassword']) ? (int)(bool)$body['mustChangePassword'] : 1;
|
|
|
|
if ($username === '' || $password === '' || $role === '') {
|
|
json_error('MISSING_FIELDS', 'username, password, and role are required', 400);
|
|
}
|
|
if (!in_array($role, ['admin', 'supervisor', 'coach'], true)) {
|
|
json_error('INVALID_ROLE', 'role must be admin, supervisor, or coach', 400);
|
|
}
|
|
if (strlen($password) < 6) {
|
|
json_error('PASSWORD_TOO_SHORT', 'Password must be at least 6 characters', 400);
|
|
}
|
|
if ($callerRole === 'supervisor' && $role !== 'coach') {
|
|
json_error('FORBIDDEN', 'Supervisors may only create coaches', 403);
|
|
}
|
|
if ($callerRole === 'supervisor') {
|
|
$supervisorID = $callerEntityID;
|
|
}
|
|
if ($role === 'coach' && $supervisorID === '') {
|
|
json_error('MISSING_FIELDS', 'supervisorID is required for coaches', 400);
|
|
}
|
|
|
|
try {
|
|
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
|
|
|
$chk = $pdo->prepare("SELECT userID FROM users WHERE username = :u");
|
|
$chk->execute([':u' => $username]);
|
|
if ($chk->fetch()) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('DUPLICATE', 'Username already exists', 409);
|
|
}
|
|
|
|
if ($role === 'coach') {
|
|
$svCheck = $pdo->prepare("SELECT supervisorID FROM supervisor WHERE supervisorID = :sid");
|
|
$svCheck->execute([':sid' => $supervisorID]);
|
|
if (!$svCheck->fetch()) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('NOT_FOUND', 'Supervisor not found', 400);
|
|
}
|
|
if ($callerRole === 'supervisor' && $supervisorID !== $callerEntityID) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Cannot create coaches for another supervisor', 403);
|
|
}
|
|
}
|
|
|
|
$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);
|
|
|
|
$svUsername = null;
|
|
if ($role === 'coach') {
|
|
[$pdo2, $tmp2, $lk2] = qdb_open_read_or_fail();
|
|
$svr = $pdo2->prepare("SELECT username FROM supervisor WHERE supervisorID = :sid");
|
|
$svr->execute([':sid' => $supervisorID]);
|
|
$svUsername = $svr->fetchColumn() ?: null;
|
|
qdb_discard($tmp2, $lk2);
|
|
}
|
|
|
|
json_success([
|
|
'userID' => $userID,
|
|
'entityID' => $entityID,
|
|
'username' => $username,
|
|
'role' => $role,
|
|
'location' => $location,
|
|
'supervisorID' => $role === 'coach' ? $supervisorID : null,
|
|
'supervisorUsername' => $svUsername,
|
|
'mustChangePassword' => $mustChange,
|
|
'createdAt' => $now,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'users', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
|
|
case 'PUT':
|
|
case 'PATCH':
|
|
$body = read_json_body();
|
|
$targetUserID = trim($body['userID'] ?? '');
|
|
$action = trim((string)($body['action'] ?? ''));
|
|
$newPassword = (string)($body['password'] ?? $body['newPassword'] ?? '');
|
|
|
|
if ($action === 'revokeSessions') {
|
|
if ($targetUserID === '') {
|
|
json_error('MISSING_FIELDS', 'userID is required', 400);
|
|
}
|
|
if ($targetUserID === ($tokenRec['userID'] ?? '')) {
|
|
json_error('FORBIDDEN', 'Use Admin settings to revoke your own other sessions, or sign out', 400);
|
|
}
|
|
|
|
try {
|
|
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
|
|
|
$row = $pdo->prepare('SELECT userID, username, role, entityID FROM users WHERE userID = :uid');
|
|
$row->execute([':uid' => $targetUserID]);
|
|
$target = $row->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$target) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('NOT_FOUND', 'User not found', 404);
|
|
}
|
|
|
|
$targetRole = $target['role'] ?? '';
|
|
if ($callerRole === 'admin') {
|
|
if (!in_array($targetRole, ['admin', 'supervisor', 'coach'], true)) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Cannot revoke sessions for this user', 403);
|
|
}
|
|
} elseif ($callerRole === 'supervisor') {
|
|
if ($targetRole !== 'coach') {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Supervisors may only revoke sessions for their coaches', 403);
|
|
}
|
|
$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);
|
|
json_error('FORBIDDEN', 'Coach is not under your supervision', 403);
|
|
}
|
|
} else {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Not allowed to revoke sessions', 403);
|
|
}
|
|
|
|
$removed = token_revoke_all_for_user_on_pdo($pdo, $targetUserID);
|
|
qdb_save($tmpDb, $lockFp);
|
|
|
|
json_success([
|
|
'userID' => $targetUserID,
|
|
'username' => $target['username'],
|
|
'revokedSessions' => $removed,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'Revoke user sessions', null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ($newPassword !== '') {
|
|
if ($targetUserID === '') {
|
|
json_error('MISSING_FIELDS', 'userID is required', 400);
|
|
}
|
|
if ($targetUserID === ($tokenRec['userID'] ?? '')) {
|
|
json_error('FORBIDDEN', 'Cannot reset your own password', 400);
|
|
}
|
|
if (strlen($newPassword) < 6) {
|
|
json_error('PASSWORD_TOO_SHORT', 'Password must be at least 6 characters', 400);
|
|
}
|
|
$mustChange = isset($body['mustChangePassword']) ? (int)(bool)$body['mustChangePassword'] : 1;
|
|
|
|
try {
|
|
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
|
|
|
$row = $pdo->prepare(
|
|
"SELECT u.userID, u.username, u.role, u.entityID, c.supervisorID
|
|
FROM users u
|
|
LEFT JOIN coach c ON c.coachID = u.entityID AND u.role = 'coach'
|
|
WHERE u.userID = :uid"
|
|
);
|
|
$row->execute([':uid' => $targetUserID]);
|
|
$target = $row->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$target) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('NOT_FOUND', 'User not found', 404);
|
|
}
|
|
|
|
$targetRole = $target['role'] ?? '';
|
|
if ($callerRole === 'admin') {
|
|
if (!in_array($targetRole, ['supervisor', 'coach'], true)) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Admins may only reset passwords for supervisors and coaches', 403);
|
|
}
|
|
} elseif ($callerRole === 'supervisor') {
|
|
if ($targetRole !== 'coach') {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Supervisors may only reset passwords for their coaches', 403);
|
|
}
|
|
$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);
|
|
json_error('FORBIDDEN', 'Coach is not under your supervision', 403);
|
|
}
|
|
} else {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Not allowed to reset passwords', 403);
|
|
}
|
|
|
|
$hash = password_hash($newPassword, PASSWORD_DEFAULT);
|
|
$pdo->prepare(
|
|
'UPDATE users SET passwordHash = :h, mustChangePassword = :mcp WHERE userID = :uid'
|
|
)->execute([':h' => $hash, ':mcp' => $mustChange, ':uid' => $targetUserID]);
|
|
|
|
token_revoke_all_for_user_on_pdo($pdo, $targetUserID);
|
|
|
|
qdb_save($tmpDb, $lockFp);
|
|
|
|
json_success([
|
|
'userID' => $targetUserID,
|
|
'username' => $target['username'],
|
|
'mustChangePassword' => $mustChange,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'users', null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ($callerRole !== 'admin') {
|
|
json_error('FORBIDDEN', 'Only admins can reassign coaches', 403);
|
|
}
|
|
|
|
$supervisorID = trim($body['supervisorID'] ?? '');
|
|
|
|
if ($targetUserID === '' || $supervisorID === '') {
|
|
json_error('MISSING_FIELDS', 'userID and supervisorID are required', 400);
|
|
}
|
|
|
|
try {
|
|
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
|
|
|
$row = $pdo->prepare(
|
|
"SELECT u.userID, u.username, u.role, u.entityID, c.supervisorID
|
|
FROM users u
|
|
LEFT JOIN coach c ON c.coachID = u.entityID AND u.role = 'coach'
|
|
WHERE u.userID = :uid"
|
|
);
|
|
$row->execute([':uid' => $targetUserID]);
|
|
$target = $row->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$target) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('NOT_FOUND', 'User not found', 404);
|
|
}
|
|
if (($target['role'] ?? '') !== 'coach') {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('INVALID_FIELD', 'Only coaches can be reassigned to a supervisor', 400);
|
|
}
|
|
|
|
$svCheck = $pdo->prepare('SELECT username FROM supervisor WHERE supervisorID = :sid');
|
|
$svCheck->execute([':sid' => $supervisorID]);
|
|
$svUsername = $svCheck->fetchColumn();
|
|
if ($svUsername === false) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('NOT_FOUND', 'Supervisor not found', 404);
|
|
}
|
|
|
|
if (($target['supervisorID'] ?? '') === $supervisorID) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_success([
|
|
'userID' => $targetUserID,
|
|
'supervisorID' => $supervisorID,
|
|
'supervisorUsername' => $svUsername,
|
|
]);
|
|
}
|
|
|
|
$pdo->prepare('UPDATE coach SET supervisorID = :sid WHERE coachID = :cid')
|
|
->execute([':sid' => $supervisorID, ':cid' => $target['entityID']]);
|
|
|
|
qdb_save($tmpDb, $lockFp);
|
|
|
|
json_success([
|
|
'userID' => $targetUserID,
|
|
'supervisorID' => $supervisorID,
|
|
'supervisorUsername' => $svUsername,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'users', null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
|
|
case 'DELETE':
|
|
$body = read_json_body();
|
|
$targetUserID = trim($body['userID'] ?? '');
|
|
|
|
if ($targetUserID === '') {
|
|
json_error('MISSING_FIELDS', 'userID is required', 400);
|
|
}
|
|
if ($targetUserID === ($tokenRec['userID'] ?? '')) {
|
|
json_error('FORBIDDEN', 'Cannot delete your own account', 400);
|
|
}
|
|
|
|
try {
|
|
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
|
|
|
$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);
|
|
json_error('NOT_FOUND', 'User not found', 404);
|
|
}
|
|
|
|
if ($callerRole === 'supervisor') {
|
|
if ($target['role'] !== 'coach') {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Supervisors can only delete coaches', 403);
|
|
}
|
|
$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);
|
|
json_error('FORBIDDEN', 'Coach is not under your supervision', 403);
|
|
}
|
|
}
|
|
|
|
$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);
|
|
|
|
json_success([]);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'users', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
|
}
|