From 9c887537e8521e510f12b936a15f5c9b01de0384 Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Wed, 27 May 2026 18:32:00 +0200 Subject: [PATCH] removed legacy files --- api/answer_options.php | 185 ----- api/app_questionnaires.php | 160 ---- api/assignments.php | 116 --- api/export.php | 152 ---- api/index.php | 1 - api/logout.php | 21 - api/questionnaires.php | 169 ----- api/questions.php | 215 ------ api/results.php | 143 ---- api/translations.php | 291 -------- api/users.php | 274 ------- assign_client.php | 117 --- backup.php | 42 -- change_password.php | 113 --- checkDatabaseExists.php | 10 - db_download.php | 46 -- db_view.php | 78 -- db_view_ordered.php | 156 ---- dev-router.php | 38 - docs/android-questionnaire-api.md | 28 +- downloadFull.php | 51 -- handlers/app_questionnaires.php | 6 +- handlers/auth.php | 18 +- handlers/download.php | 73 -- header_order.json | 106 --- import_questionnaires.php | 195 ----- index.html | 975 ------------------------- lib/encrypted_payload.php | 25 + lib/repositories/ClientRepo.php | 102 --- lib/repositories/QuestionRepo.php | 146 ---- lib/repositories/QuestionnaireRepo.php | 178 ----- lib/repositories/ResultRepo.php | 124 ---- lib/repositories/SessionRepo.php | 61 -- lib/repositories/TranslationRepo.php | 103 --- lib/repositories/UserRepo.php | 211 ------ lib/response.php | 25 + login.php | 85 --- manage_users.php | 236 ------ questions_en.json | 104 --- seed_user.php | 176 ----- tokens.jsonl | 24 - translations_en.json | 349 --------- uploadDeltaTest5.php | 356 --------- users.json | 50 -- valid_tokens.txt | 24 - 45 files changed, 91 insertions(+), 6067 deletions(-) delete mode 100644 api/answer_options.php delete mode 100644 api/app_questionnaires.php delete mode 100644 api/assignments.php delete mode 100644 api/export.php delete mode 100644 api/logout.php delete mode 100644 api/questionnaires.php delete mode 100644 api/questions.php delete mode 100644 api/results.php delete mode 100644 api/translations.php delete mode 100644 api/users.php delete mode 100644 assign_client.php delete mode 100644 backup.php delete mode 100644 change_password.php delete mode 100755 checkDatabaseExists.php delete mode 100644 db_download.php delete mode 100644 db_view.php delete mode 100644 db_view_ordered.php delete mode 100644 dev-router.php delete mode 100755 downloadFull.php delete mode 100644 handlers/download.php delete mode 100644 header_order.json delete mode 100644 import_questionnaires.php delete mode 100644 index.html create mode 100644 lib/encrypted_payload.php delete mode 100644 lib/repositories/ClientRepo.php delete mode 100644 lib/repositories/QuestionRepo.php delete mode 100644 lib/repositories/QuestionnaireRepo.php delete mode 100644 lib/repositories/ResultRepo.php delete mode 100644 lib/repositories/SessionRepo.php delete mode 100644 lib/repositories/TranslationRepo.php delete mode 100644 lib/repositories/UserRepo.php delete mode 100644 login.php delete mode 100644 manage_users.php delete mode 100644 questions_en.json delete mode 100644 seed_user.php delete mode 100644 tokens.jsonl delete mode 100644 translations_en.json delete mode 100644 uploadDeltaTest5.php delete mode 100644 users.json delete mode 100644 valid_tokens.txt diff --git a/api/answer_options.php b/api/answer_options.php deleted file mode 100644 index 4c48690..0000000 --- a/api/answer_options.php +++ /dev/null @@ -1,185 +0,0 @@ - "questionID query param required"]); - exit; - } - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - $stmt = $pdo->prepare(" - SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId - FROM answer_option WHERE questionID = :qid ORDER BY orderIndex - "); - $stmt->execute([':qid' => $qID]); - $options = $stmt->fetchAll(PDO::FETCH_ASSOC); - foreach ($options as &$o) { - $o['points'] = (int)$o['points']; - $o['orderIndex'] = (int)$o['orderIndex']; - $tr = $pdo->prepare("SELECT languageCode, text FROM answer_option_translation WHERE answerOptionID = :id"); - $tr->execute([':id' => $o['answerOptionID']]); - $o['translations'] = $tr->fetchAll(PDO::FETCH_ASSOC); - } - unset($o); - qdb_discard($tmpDb, $lockFp); - echo json_encode(["success" => true, "answerOptions" => $options]); - break; - -case 'POST': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionID']) || !isset($body['defaultText'])) { - http_response_code(400); - echo json_encode(["error" => "questionID and defaultText required"]); - exit; - } - - $id = bin2hex(random_bytes(16)); - $qID = $body['questionID']; - $text = trim($body['defaultText']); - $points = (int)($body['points'] ?? 0); - $order = (int)($body['orderIndex'] ?? 0); - $nextQ = trim($body['nextQuestionId'] ?? ''); - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $chk = $pdo->prepare("SELECT 1 FROM question WHERE questionID = :id"); - $chk->execute([':id' => $qID]); - if (!$chk->fetch()) { - qdb_discard($tmpDb, $lockFp); - http_response_code(404); - echo json_encode(["error" => "Question not found"]); - exit; - } - if ($order === 0) { - $max = $pdo->prepare("SELECT COALESCE(MAX(orderIndex),0)+1 FROM answer_option WHERE questionID = :id"); - $max->execute([':id' => $qID]); - $order = (int)$max->fetchColumn(); - } - $pdo->prepare("INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId) - VALUES (:id, :qid, :t, :p, :o, :nq)") - ->execute([':id' => $id, ':qid' => $qID, ':t' => $text, ':p' => $points, ':o' => $order, ':nq' => $nextQ]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true, "answerOption" => [ - "answerOptionID" => $id, "questionID" => $qID, "defaultText" => $text, - "points" => $points, "orderIndex" => $order, "nextQuestionId" => $nextQ, "translations" => [] - ]]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'PUT': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['answerOptionID'])) { - http_response_code(400); - echo json_encode(["error" => "answerOptionID is required"]); - exit; - } - $id = $body['answerOptionID']; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $existing = $pdo->prepare("SELECT * FROM answer_option WHERE answerOptionID = :id"); - $existing->execute([':id' => $id]); - $row = $existing->fetch(PDO::FETCH_ASSOC); - if (!$row) { - qdb_discard($tmpDb, $lockFp); - http_response_code(404); - echo json_encode(["error" => "Answer option not found"]); - exit; - } - $text = trim($body['defaultText'] ?? $row['defaultText']); - $points = (int)($body['points'] ?? $row['points']); - $order = (int)($body['orderIndex'] ?? $row['orderIndex']); - $nextQ = trim($body['nextQuestionId'] ?? $row['nextQuestionId']); - - $pdo->prepare("UPDATE answer_option SET defaultText = :t, points = :p, orderIndex = :o, nextQuestionId = :nq WHERE answerOptionID = :id") - ->execute([':t' => $text, ':p' => $points, ':o' => $order, ':nq' => $nextQ, ':id' => $id]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true, "answerOption" => [ - "answerOptionID" => $id, "questionID" => $row['questionID'], - "defaultText" => $text, "points" => $points, "orderIndex" => $order, - "nextQuestionId" => $nextQ - ]]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'DELETE': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['answerOptionID'])) { - http_response_code(400); - echo json_encode(["error" => "answerOptionID is required"]); - exit; - } - $id = $body['answerOptionID']; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $pdo->exec("PRAGMA foreign_keys = OFF;"); - $pdo->beginTransaction(); - $pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id")->execute([':id' => $id]); - $pdo->prepare("UPDATE client_answer SET answerOptionID = NULL WHERE answerOptionID = :id")->execute([':id' => $id]); - $pdo->prepare("DELETE FROM answer_option WHERE answerOptionID = :id")->execute([':id' => $id]); - $pdo->commit(); - $pdo->exec("PRAGMA foreign_keys = ON;"); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - if ($pdo->inTransaction()) $pdo->rollBack(); - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'PATCH': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionID']) || !is_array($body['order'] ?? null)) { - http_response_code(400); - echo json_encode(["error" => "questionID and order[] required"]); - exit; - } - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $stmt = $pdo->prepare("UPDATE answer_option SET orderIndex = :o WHERE answerOptionID = :id AND questionID = :qid"); - foreach ($body['order'] as $idx => $aoid) { - $stmt->execute([':o' => $idx, ':id' => $aoid, ':qid' => $body['questionID']]); - } - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -default: - http_response_code(405); - echo json_encode(["error" => "Method not allowed"]); -} diff --git a/api/app_questionnaires.php b/api/app_questionnaires.php deleted file mode 100644 index 1005a54..0000000 --- a/api/app_questionnaires.php +++ /dev/null @@ -1,160 +0,0 @@ - ordered questionnaire list with conditions - * GET ?id= -> single questionnaire in app JSON format - * GET ?translations=1 -> global app UI strings only; **no auth** (login screen). - * - * POST (coach interview upload) is handled by api/index.php -> handlers/app_questionnaires.php - */ -require_once __DIR__ . '/../common.php'; -require_once __DIR__ . '/../db_init.php'; -require_once __DIR__ . '/../lib/response.php'; - -header('Content-Type: application/json; charset=UTF-8'); - -$method = $_SERVER['REQUEST_METHOD']; - -if ($method === 'GET' && !empty($_GET['translations'])) { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - json_success(['translations' => qdb_build_app_translations_map($pdo)]); - qdb_discard($tmpDb, $lockFp); - exit; -} - -$tokenRec = require_valid_token(); - -if ($method !== 'GET') { - json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405); -} - -[$pdo, $tmpDb, $lockFp] = qdb_open(false); - -$qnID = $_GET['id'] ?? ''; - -if ($qnID) { - // Single questionnaire in app JSON format - $qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id"); - $qn->execute([':id' => $qnID]); - $qnRow = $qn->fetch(PDO::FETCH_ASSOC); - if (!$qnRow) { - qdb_discard($tmpDb, $lockFp); - http_response_code(404); - echo json_encode(["error" => "Questionnaire not found"]); - exit; - } - - $stmt = $pdo->prepare(" - SELECT questionID, defaultText, type, orderIndex, configJson - FROM question WHERE questionnaireID = :id ORDER BY orderIndex - "); - $stmt->execute([':id' => $qnID]); - $dbQuestions = $stmt->fetchAll(PDO::FETCH_ASSOC); - - $questions = []; - foreach ($dbQuestions as $dbQ) { - $localId = $dbQ['questionID']; - $parts = explode('__', $localId); - $shortId = end($parts); - - $layout = $dbQ['type']; - $config = qdb_parse_config_json($dbQ['configJson']); - $qKey = qdb_question_key($config, $dbQ['defaultText']); - - $q = [ - 'id' => $shortId, - 'layout' => $layout, - 'question' => $qKey !== '' ? $qKey : $dbQ['defaultText'], - ]; - - if ($qKey !== '' && !empty($config['noteBefore'])) { - $q['noteBeforeKey'] = qdb_note_before_key($qKey); - } - if ($qKey !== '' && !empty($config['noteAfter'])) { - $q['noteAfterKey'] = qdb_note_after_key($qKey); - } - if (isset($config['textKey'])) $q['textKey'] = $config['textKey']; - if (isset($config['textKey1'])) $q['textKey1'] = $config['textKey1']; - if (isset($config['textKey2'])) $q['textKey2'] = $config['textKey2']; - if (isset($config['hint'])) $q['hint'] = $config['hint']; - if (isset($config['hint1'])) $q['hint1'] = $config['hint1']; - if (isset($config['hint2'])) $q['hint2'] = $config['hint2']; - if (isset($config['symptoms'])) $q['symptoms'] = $config['symptoms']; - if (isset($config['scaleType'])) $q['scaleType'] = $config['scaleType']; - if (isset($config['range'])) $q['range'] = $config['range']; - if (isset($config['step'])) $q['step'] = (int)$config['step']; - if (isset($config['unitLabel'])) $q['unitLabel'] = $config['unitLabel']; - if (isset($config['constraints'])) $q['constraints'] = $config['constraints']; - if (isset($config['precision'])) $q['precision'] = $config['precision']; - if (isset($config['minSelection'])) $q['minSelection'] = $config['minSelection']; - if (isset($config['maxLength'])) $q['maxLength'] = (int)$config['maxLength']; - if (isset($config['nextQuestionId'])) $q['nextQuestionId'] = $config['nextQuestionId']; - if (isset($config['otherNextQuestionId'])) $q['otherNextQuestionId'] = $config['otherNextQuestionId']; - if (isset($config['otherOptionKey'])) $q['otherOptionKey'] = $config['otherOptionKey']; - - if ($layout === 'string_spinner' && isset($config['options'])) { - $q['options'] = $config['options']; - } - if (($layout === 'value_spinner' || $layout === 'slider_question') && isset($config['valueOptions'])) { - $q['options'] = $config['valueOptions']; - } - - // Answer options (for radio_question, multi_check_box_question, etc.) - $ao = $pdo->prepare(" - SELECT defaultText, points, nextQuestionId - FROM answer_option WHERE questionID = :qid ORDER BY orderIndex - "); - $ao->execute([':qid' => $dbQ['questionID']]); - $opts = $ao->fetchAll(PDO::FETCH_ASSOC); - - if ($opts) { - $optionsArr = []; - $pointsMap = []; - foreach ($opts as $opt) { - $o = ['key' => $opt['defaultText']]; - if ($opt['nextQuestionId'] !== '') { - $o['nextQuestionId'] = $opt['nextQuestionId']; - } - $optionsArr[] = $o; - $pointsMap[$opt['defaultText']] = (int)$opt['points']; - } - $q['options'] = $optionsArr; - - $hasNonZero = false; - foreach ($pointsMap as $v) { if ($v !== 0) { $hasNonZero = true; break; } } - $q['pointsMap'] = $pointsMap; - } - - $questions[] = $q; - } - - qdb_discard($tmpDb, $lockFp); - echo json_encode([ - 'meta' => ['id' => $qnID], - 'questions' => $questions, - 'translations' => qdb_build_questionnaire_translations_map($pdo, $qnID), - ]); - exit; -} - -// Default: ordered questionnaire list -$rows = $pdo->query(" - SELECT questionnaireID, name, showPoints, conditionJson - FROM questionnaire - WHERE state = 'active' - ORDER BY orderIndex -")->fetchAll(PDO::FETCH_ASSOC); - -$list = []; -foreach ($rows as $r) { - $list[] = [ - 'id' => $r['questionnaireID'], - 'name' => $r['name'], - 'showPoints' => (bool)(int)$r['showPoints'], - 'condition' => json_decode($r['conditionJson'], true) ?: new stdClass(), - ]; -} - -qdb_discard($tmpDb, $lockFp); -echo json_encode($list); diff --git a/api/assignments.php b/api/assignments.php deleted file mode 100644 index cc36db4..0000000 --- a/api/assignments.php +++ /dev/null @@ -1,116 +0,0 @@ -query( - "SELECT c.coachID, c.username, c.supervisorID, sv.username AS supervisorUsername - FROM coach c - LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID - ORDER BY sv.username, c.username" - )->fetchAll(PDO::FETCH_ASSOC); - } else { - $stmt = $pdo->prepare( - "SELECT c.coachID, c.username, c.supervisorID, sv.username AS supervisorUsername - FROM coach c - LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID - WHERE c.supervisorID = :sid ORDER BY c.username" - ); - $stmt->execute([':sid' => $callerEntityID]); - $coaches = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - [$clause, $params] = rbac_client_filter($tokenRec); - $clientStmt = $pdo->prepare( - "SELECT cl.clientCode, cl.coachID, co.username AS coachUsername - FROM client cl - LEFT JOIN coach co ON co.coachID = cl.coachID - WHERE $clause - ORDER BY cl.coachID, cl.clientCode" - ); - foreach ($params as $k => $v) $clientStmt->bindValue($k, $v); - $clientStmt->execute(); - $clients = $clientStmt->fetchAll(PDO::FETCH_ASSOC); - - qdb_discard($tmpDb, $lockFp); - echo json_encode(["success" => true, "coaches" => $coaches, "clients" => $clients]); - } 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: assign clients to a coach ────────────────────────────────────── -if ($method === 'POST') { - $in = json_decode(file_get_contents('php://input'), true) ?: []; - $clientCodes = $in['clientCodes'] ?? []; - $coachID = trim($in['coachID'] ?? ''); - - if (empty($clientCodes) || $coachID === '') { - http_response_code(400); - echo json_encode(["error" => "clientCodes and coachID required"]); - exit; - } - if (!is_array($clientCodes)) $clientCodes = [$clientCodes]; - - try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - - if ($callerRole === 'supervisor') { - $chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :sid"); - $chk->execute([':cid' => $coachID, ':sid' => $callerEntityID]); - } else { - $chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid"); - $chk->execute([':cid' => $coachID]); - } - if (!$chk->fetch()) { - qdb_discard($tmpDb, $lockFp); - http_response_code(400); - echo json_encode(["error" => "Coach not found or not authorized"]); - exit; - } - - // Only allow reassignment of clients the caller can already see - [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec); - $pdo->beginTransaction(); - $updStmt = $pdo->prepare( - "UPDATE client SET coachID = :cid WHERE clientCode = :cc AND ($rbacClause)" - ); - $count = 0; - foreach ($clientCodes as $cc) { - $cc = trim($cc); - if ($cc === '') continue; - $updStmt->execute(array_merge([':cid' => $coachID, ':cc' => $cc], $rbacParams)); - $count += $updStmt->rowCount(); - } - $pdo->commit(); - qdb_save($tmpDb, $lockFp); - - echo json_encode(["success" => true, "assigned" => $count]); - } 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"]); diff --git a/api/export.php b/api/export.php deleted file mode 100644 index 257b0b9..0000000 --- a/api/export.php +++ /dev/null @@ -1,152 +0,0 @@ - "Method not allowed"]); - exit; -} - -if (!empty($_GET['bundle'])) { - $role = $tokenRec['role'] ?? ''; - if (!in_array($role, ['admin', 'supervisor'], true)) { - header('Content-Type: application/json; charset=UTF-8'); - http_response_code(403); - echo json_encode(["error" => "Permission denied"]); - exit; - } - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - $bundle = qdb_export_all_questionnaires_bundle($pdo); - qdb_discard($tmpDb, $lockFp); - $filename = 'questionnaires_bundle_' . date('Y-m-d_His') . '.json'; - header('Content-Type: application/json; charset=UTF-8'); - header('Content-Disposition: attachment; filename="' . $filename . '"'); - echo json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - exit; -} - -$qnID = $_GET['questionnaireID'] ?? ''; -$format = strtolower($_GET['format'] ?? 'csv'); - -if (!$qnID) { - header('Content-Type: application/json; charset=UTF-8'); - http_response_code(400); - echo json_encode(["error" => "questionnaireID query param required"]); - exit; -} - -[$pdo, $tmpDb, $lockFp] = qdb_open(false); - -// Questionnaire metadata -$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id"); -$qn->execute([':id' => $qnID]); -$questionnaire = $qn->fetch(PDO::FETCH_ASSOC); -if (!$questionnaire) { - qdb_discard($tmpDb, $lockFp); - header('Content-Type: application/json; charset=UTF-8'); - http_response_code(404); - echo json_encode(["error" => "Questionnaire not found"]); - exit; -} - -// Questions ordered -$qStmt = $pdo->prepare("SELECT questionID, defaultText, type, orderIndex, configJson FROM question WHERE questionnaireID = :id ORDER BY orderIndex"); -$qStmt->execute([':id' => $qnID]); -$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC); -$questionIDs = array_column($questions, 'questionID'); -$resultColumns = qdb_results_export_columns($questions, $qnID); - -// Build answer-option lookup for resolving display text -$optionTextMap = []; -foreach ($questionIDs as $qid) { - $aoStmt = $pdo->prepare("SELECT answerOptionID, defaultText FROM answer_option WHERE questionID = :qid"); - $aoStmt->execute([':qid' => $qid]); - foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) { - $optionTextMap[$ao['answerOptionID']] = $ao['defaultText']; - } -} - -// RBAC filter -[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); - -$sql = " - SELECT cl.clientCode, cl.coachID, - co.username AS coachUsername, - sv.username AS supervisorUsername, - cq.status, cq.sumPoints, cq.startedAt, cq.completedAt - FROM client cl - INNER JOIN completed_questionnaire cq ON cq.clientCode = cl.clientCode AND cq.questionnaireID = :qnid - LEFT JOIN coach co ON co.coachID = cl.coachID - LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID - WHERE $rbacClause - ORDER BY cl.clientCode -"; -$params = array_merge([':qnid' => $qnID], $rbacParams); -$cStmt = $pdo->prepare($sql); -$cStmt->execute($params); -$clients = $cStmt->fetchAll(PDO::FETCH_ASSOC); - -// Fetch answers for each client -$qPlaceholders = !empty($questionIDs) ? implode(',', array_fill(0, count($questionIDs), '?')) : "'__none__'"; -$answerStmt = $pdo->prepare(" - SELECT questionID, answerOptionID, freeTextValue, numericValue - FROM client_answer - WHERE clientCode = ? AND questionID IN ($qPlaceholders) -"); - -$rows = []; -foreach ($clients as $c) { - $row = [ - 'clientCode' => $c['clientCode'], - 'coach' => $c['coachUsername'] ?? $c['coachID'], - 'supervisor' => $c['supervisorUsername'] ?? '', - 'status' => $c['status'], - 'sumPoints' => $c['sumPoints'], - 'startedAt' => $c['startedAt'] ? date('Y-m-d H:i', (int)$c['startedAt']) : '', - 'completedAt'=> $c['completedAt'] ? date('Y-m-d H:i', (int)$c['completedAt']) : '', - ]; - - $bindParams = array_merge([$c['clientCode']], $questionIDs); - $answerStmt->execute($bindParams); - $answers = $answerStmt->fetchAll(PDO::FETCH_ASSOC); - $answerMap = []; - foreach ($answers as $a) $answerMap[$a['questionID']] = $a; - - foreach ($resultColumns as $col) { - $qid = $col['questionID']; - $a = $answerMap[$qid] ?? null; - $row[$col['header']] = qdb_results_column_cell_value($col, $a, $optionTextMap); - } - $rows[] = $row; -} - -qdb_discard($tmpDb, $lockFp); - -// Output CSV -$safeName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $questionnaire['name']); -$filename = $safeName . '_v' . $questionnaire['version'] . '.csv'; - -header('Content-Type: text/csv; charset=UTF-8'); -header('Content-Disposition: attachment; filename="' . $filename . '"'); - -$out = fopen('php://output', 'w'); -// BOM for Excel UTF-8 recognition -fwrite($out, "\xEF\xBB\xBF"); - -if (!empty($rows)) { - fputcsv($out, array_keys($rows[0])); - foreach ($rows as $row) { - fputcsv($out, array_values($row)); - } -} else { - $header = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'startedAt', 'completedAt']; - foreach ($resultColumns as $col) { - $header[] = $col['header']; - } - fputcsv($out, $header); -} -fclose($out); diff --git a/api/index.php b/api/index.php index 6e0db7d..e767421 100644 --- a/api/index.php +++ b/api/index.php @@ -48,7 +48,6 @@ $routes = [ 'backup' => __DIR__ . '/../handlers/backup.php', 'dev' => __DIR__ . '/../handlers/dev.php', 'dev/import' => __DIR__ . '/../handlers/dev.php', - 'download' => __DIR__ . '/../handlers/download.php', ]; try { diff --git a/api/logout.php b/api/logout.php deleted file mode 100644 index f56d308..0000000 --- a/api/logout.php +++ /dev/null @@ -1,21 +0,0 @@ - "Method not allowed"]); - exit; -} - -$token = get_bearer_token(); -if (!$token) { - http_response_code(401); - echo json_encode(["error" => "Missing Bearer token"]); - exit; -} - -token_revoke($token); -echo json_encode(["success" => true]); diff --git a/api/questionnaires.php b/api/questionnaires.php deleted file mode 100644 index 6bff3da..0000000 --- a/api/questionnaires.php +++ /dev/null @@ -1,169 +0,0 @@ -query(" - SELECT q.questionnaireID, q.name, q.version, q.state, - q.orderIndex, q.showPoints, q.conditionJson, - COUNT(qu.questionID) AS questionCount - FROM questionnaire q - LEFT JOIN question qu ON qu.questionnaireID = q.questionnaireID - GROUP BY q.questionnaireID - ORDER BY q.orderIndex, q.name - ")->fetchAll(PDO::FETCH_ASSOC); - foreach ($rows as &$r) { - $r['orderIndex'] = (int)$r['orderIndex']; - $r['showPoints'] = (int)$r['showPoints']; - $r['conditionJson'] = $r['conditionJson'] ?: '{}'; - } - unset($r); - qdb_discard($tmpDb, $lockFp); - echo json_encode(["success" => true, "questionnaires" => $rows]); - break; - -case 'POST': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['name'])) { - http_response_code(400); - echo json_encode(["error" => "name is required"]); - exit; - } - $id = bin2hex(random_bytes(16)); - $name = trim($body['name']); - $version = trim($body['version'] ?? ''); - $state = trim($body['state'] ?? 'draft'); - $order = (int)($body['orderIndex'] ?? 0); - $showPts = (int)($body['showPoints'] ?? 0); - $condJson = $body['conditionJson'] ?? '{}'; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - if ($order === 0) { - $order = (int)$pdo->query("SELECT COALESCE(MAX(orderIndex),0)+1 FROM questionnaire")->fetchColumn(); - } - $pdo->prepare("INSERT INTO questionnaire (questionnaireID, name, version, state, orderIndex, showPoints, conditionJson) - VALUES (:id, :n, :v, :s, :o, :sp, :cj)") - ->execute([':id' => $id, ':n' => $name, ':v' => $version, ':s' => $state, - ':o' => $order, ':sp' => $showPts, ':cj' => $condJson]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true, "questionnaire" => [ - "questionnaireID" => $id, "name" => $name, "version" => $version, - "state" => $state, "orderIndex" => $order, "showPoints" => $showPts, - "conditionJson" => $condJson, "questionCount" => 0 - ]]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'PUT': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionnaireID'])) { - http_response_code(400); - echo json_encode(["error" => "questionnaireID is required"]); - exit; - } - $id = $body['questionnaireID']; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $existing = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id"); - $existing->execute([':id' => $id]); - $row = $existing->fetch(PDO::FETCH_ASSOC); - if (!$row) { - qdb_discard($tmpDb, $lockFp); - http_response_code(404); - echo json_encode(["error" => "Questionnaire not found"]); - exit; - } - $name = trim($body['name'] ?? $row['name']); - $version = trim($body['version'] ?? $row['version']); - $state = trim($body['state'] ?? $row['state']); - $order = (int)($body['orderIndex'] ?? $row['orderIndex']); - $showPts = (int)($body['showPoints'] ?? $row['showPoints']); - $condJson = $body['conditionJson'] ?? $row['conditionJson']; - - $pdo->prepare("UPDATE questionnaire SET name = :n, version = :v, state = :s, - orderIndex = :o, showPoints = :sp, conditionJson = :cj - WHERE questionnaireID = :id") - ->execute([':n' => $name, ':v' => $version, ':s' => $state, - ':o' => $order, ':sp' => $showPts, ':cj' => $condJson, ':id' => $id]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true, "questionnaire" => [ - "questionnaireID" => $id, "name" => $name, "version" => $version, "state" => $state, - "orderIndex" => $order, "showPoints" => $showPts, "conditionJson" => $condJson - ]]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'DELETE': - require_role(['admin'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionnaireID'])) { - http_response_code(400); - echo json_encode(["error" => "questionnaireID is required"]); - exit; - } - $id = $body['questionnaireID']; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $pdo->exec("PRAGMA foreign_keys = OFF;"); - $pdo->beginTransaction(); - - // Get question IDs to cascade-delete answer options and translations - $qIds = $pdo->prepare("SELECT questionID FROM question WHERE questionnaireID = :id"); - $qIds->execute([':id' => $id]); - $questionIDs = $qIds->fetchAll(PDO::FETCH_COLUMN); - - foreach ($questionIDs as $qid) { - $aoIds = $pdo->prepare("SELECT answerOptionID FROM answer_option WHERE questionID = :qid"); - $aoIds->execute([':qid' => $qid]); - $optionIDs = $aoIds->fetchAll(PDO::FETCH_COLUMN); - foreach ($optionIDs as $aoid) { - $pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id")->execute([':id' => $aoid]); - } - $pdo->prepare("DELETE FROM answer_option WHERE questionID = :qid")->execute([':qid' => $qid]); - $pdo->prepare("DELETE FROM question_translation WHERE questionID = :qid")->execute([':qid' => $qid]); - $pdo->prepare("DELETE FROM client_answer WHERE questionID = :qid")->execute([':qid' => $qid]); - } - $pdo->prepare("DELETE FROM question WHERE questionnaireID = :id")->execute([':id' => $id]); - $pdo->prepare("DELETE FROM completed_questionnaire WHERE questionnaireID = :id")->execute([':id' => $id]); - $pdo->prepare("DELETE FROM questionnaire WHERE questionnaireID = :id")->execute([':id' => $id]); - - $pdo->commit(); - $pdo->exec("PRAGMA foreign_keys = ON;"); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - if ($pdo->inTransaction()) $pdo->rollBack(); - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -default: - http_response_code(405); - echo json_encode(["error" => "Method not allowed"]); -} diff --git a/api/questions.php b/api/questions.php deleted file mode 100644 index d5c9461..0000000 --- a/api/questions.php +++ /dev/null @@ -1,215 +0,0 @@ - "questionnaireID query param required"]); - exit; - } - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - - $stmt = $pdo->prepare(" - SELECT questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson - FROM question WHERE questionnaireID = :qid ORDER BY orderIndex - "); - $stmt->execute([':qid' => $qnID]); - $questions = $stmt->fetchAll(PDO::FETCH_ASSOC); - - foreach ($questions as &$q) { - $q['isRequired'] = (int)$q['isRequired']; - $q['orderIndex'] = (int)$q['orderIndex']; - $q['configJson'] = $q['configJson'] ?: '{}'; - $ao = $pdo->prepare(" - SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId - FROM answer_option WHERE questionID = :qid ORDER BY orderIndex - "); - $ao->execute([':qid' => $q['questionID']]); - $q['answerOptions'] = $ao->fetchAll(PDO::FETCH_ASSOC); - foreach ($q['answerOptions'] as &$opt) { - $opt['points'] = (int)$opt['points']; - $opt['orderIndex'] = (int)$opt['orderIndex']; - } - - $tr = $pdo->prepare("SELECT languageCode, text FROM question_translation WHERE questionID = :qid"); - $tr->execute([':qid' => $q['questionID']]); - $q['translations'] = $tr->fetchAll(PDO::FETCH_ASSOC); - } - unset($q); - - qdb_discard($tmpDb, $lockFp); - echo json_encode(["success" => true, "questions" => $questions]); - break; - -case 'POST': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionnaireID']) || !isset($body['defaultText'])) { - http_response_code(400); - echo json_encode(["error" => "questionnaireID and defaultText required"]); - exit; - } - - $id = bin2hex(random_bytes(16)); - $qnID = $body['questionnaireID']; - $text = trim($body['defaultText']); - $type = trim($body['type'] ?? ''); - $order = (int)($body['orderIndex'] ?? 0); - $req = (int)($body['isRequired'] ?? 0); - $config = $body['configJson'] ?? '{}'; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $chk = $pdo->prepare("SELECT 1 FROM questionnaire WHERE questionnaireID = :id"); - $chk->execute([':id' => $qnID]); - if (!$chk->fetch()) { - qdb_discard($tmpDb, $lockFp); - http_response_code(404); - echo json_encode(["error" => "Questionnaire not found"]); - exit; - } - - if ($order === 0) { - $max = $pdo->prepare("SELECT COALESCE(MAX(orderIndex),0)+1 FROM question WHERE questionnaireID = :id"); - $max->execute([':id' => $qnID]); - $order = (int)$max->fetchColumn(); - } - - $pdo->prepare("INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson) - VALUES (:id, :qid, :t, :ty, :o, :r, :cj)") - ->execute([':id' => $id, ':qid' => $qnID, ':t' => $text, ':ty' => $type, - ':o' => $order, ':r' => $req, ':cj' => $config]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true, "question" => [ - "questionID" => $id, "questionnaireID" => $qnID, "defaultText" => $text, - "type" => $type, "orderIndex" => $order, "isRequired" => $req, - "configJson" => $config, "answerOptions" => [], "translations" => [] - ]]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'PUT': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionID'])) { - http_response_code(400); - echo json_encode(["error" => "questionID is required"]); - exit; - } - $id = $body['questionID']; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $existing = $pdo->prepare("SELECT * FROM question WHERE questionID = :id"); - $existing->execute([':id' => $id]); - $row = $existing->fetch(PDO::FETCH_ASSOC); - if (!$row) { - qdb_discard($tmpDb, $lockFp); - http_response_code(404); - echo json_encode(["error" => "Question not found"]); - exit; - } - - $text = trim($body['defaultText'] ?? $row['defaultText']); - $type = trim($body['type'] ?? $row['type']); - $order = (int)($body['orderIndex'] ?? $row['orderIndex']); - $req = (int)($body['isRequired'] ?? $row['isRequired']); - $config = $body['configJson'] ?? $row['configJson']; - - $pdo->prepare("UPDATE question SET defaultText = :t, type = :ty, orderIndex = :o, isRequired = :r, configJson = :cj WHERE questionID = :id") - ->execute([':t' => $text, ':ty' => $type, ':o' => $order, ':r' => $req, ':cj' => $config, ':id' => $id]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true, "question" => [ - "questionID" => $id, "questionnaireID" => $row['questionnaireID'], - "defaultText" => $text, "type" => $type, "orderIndex" => $order, "isRequired" => $req, - "configJson" => $config - ]]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'DELETE': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionID'])) { - http_response_code(400); - echo json_encode(["error" => "questionID is required"]); - exit; - } - $id = $body['questionID']; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $pdo->exec("PRAGMA foreign_keys = OFF;"); - $pdo->beginTransaction(); - - $aoIds = $pdo->prepare("SELECT answerOptionID FROM answer_option WHERE questionID = :id"); - $aoIds->execute([':id' => $id]); - foreach ($aoIds->fetchAll(PDO::FETCH_COLUMN) as $aoid) { - $pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id")->execute([':id' => $aoid]); - } - $pdo->prepare("DELETE FROM answer_option WHERE questionID = :id")->execute([':id' => $id]); - $pdo->prepare("DELETE FROM question_translation WHERE questionID = :id")->execute([':id' => $id]); - $pdo->prepare("DELETE FROM client_answer WHERE questionID = :id")->execute([':id' => $id]); - $pdo->prepare("DELETE FROM question WHERE questionID = :id")->execute([':id' => $id]); - - $pdo->commit(); - $pdo->exec("PRAGMA foreign_keys = ON;"); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - if ($pdo->inTransaction()) $pdo->rollBack(); - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'PATCH': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - if (!$body || empty($body['questionnaireID']) || !is_array($body['order'] ?? null)) { - http_response_code(400); - echo json_encode(["error" => "questionnaireID and order[] required"]); - exit; - } - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $stmt = $pdo->prepare("UPDATE question SET orderIndex = :o WHERE questionID = :id AND questionnaireID = :qid"); - foreach ($body['order'] as $idx => $qid) { - $stmt->execute([':o' => $idx, ':id' => $qid, ':qid' => $body['questionnaireID']]); - } - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -default: - http_response_code(405); - echo json_encode(["error" => "Method not allowed"]); -} diff --git a/api/results.php b/api/results.php deleted file mode 100644 index 05074bf..0000000 --- a/api/results.php +++ /dev/null @@ -1,143 +0,0 @@ - "Method not allowed"]); - exit; -} - -$qnID = $_GET['questionnaireID'] ?? ''; -$clientCode = $_GET['clientCode'] ?? ''; - -if (!$qnID) { - http_response_code(400); - echo json_encode(["error" => "questionnaireID query param required"]); - exit; -} - -[$pdo, $tmpDb, $lockFp] = qdb_open(false); - -// Questionnaire metadata -$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id"); -$qn->execute([':id' => $qnID]); -$questionnaire = $qn->fetch(PDO::FETCH_ASSOC); -if (!$questionnaire) { - qdb_discard($tmpDb, $lockFp); - http_response_code(404); - echo json_encode(["error" => "Questionnaire not found"]); - exit; -} - -// Questions with answer options -$qStmt = $pdo->prepare(" - SELECT questionID, defaultText, type, orderIndex, isRequired - FROM question WHERE questionnaireID = :id ORDER BY orderIndex -"); -$qStmt->execute([':id' => $qnID]); -$questions = $qStmt->fetchAll(PDO::FETCH_ASSOC); - -$questionIDs = array_column($questions, 'questionID'); - -foreach ($questions as &$q) { - $q['isRequired'] = (int)$q['isRequired']; - $q['orderIndex'] = (int)$q['orderIndex']; - $ao = $pdo->prepare("SELECT answerOptionID, defaultText, points, orderIndex - FROM answer_option WHERE questionID = :qid ORDER BY orderIndex"); - $ao->execute([':qid' => $q['questionID']]); - $q['answerOptions'] = $ao->fetchAll(PDO::FETCH_ASSOC); - foreach ($q['answerOptions'] as &$opt) { - $opt['points'] = (int)$opt['points']; - $opt['orderIndex'] = (int)$opt['orderIndex']; - } -} -unset($q); - -// RBAC filter -[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); - -// Build client list -if ($clientCode) { - $clientSQL = "SELECT cl.clientCode, cl.coachID FROM client cl WHERE cl.clientCode = :cc AND $rbacClause"; - $clientParams = array_merge([':cc' => $clientCode], $rbacParams); -} else { - $clientSQL = "SELECT cl.clientCode, cl.coachID FROM client cl WHERE $rbacClause"; - $clientParams = $rbacParams; -} - -// Only clients that have a completed_questionnaire entry for this questionnaire -$sql = " - SELECT cl.clientCode, cl.coachID, - co.username AS coachUsername, - sv.username AS supervisorUsername, - cq.status, cq.startedAt, cq.completedAt, cq.sumPoints, cq.assignedByCoach - FROM client cl - INNER JOIN completed_questionnaire cq ON cq.clientCode = cl.clientCode AND cq.questionnaireID = :qnid - LEFT JOIN coach co ON co.coachID = cl.coachID - LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID - WHERE $rbacClause -"; -$params = array_merge([':qnid' => $qnID], $rbacParams); - -if ($clientCode) { - $sql .= " AND cl.clientCode = :cc"; - $params[':cc'] = $clientCode; -} -$sql .= " ORDER BY cl.clientCode"; - -$cStmt = $pdo->prepare($sql); -$cStmt->execute($params); -$clients = $cStmt->fetchAll(PDO::FETCH_ASSOC); - -// Build question ID placeholders for answer fetch -if (!empty($questionIDs) && !empty($clients)) { - $qPlaceholders = implode(',', array_fill(0, count($questionIDs), '?')); - $answerStmt = $pdo->prepare(" - SELECT clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt - FROM client_answer - WHERE clientCode = ? AND questionID IN ($qPlaceholders) - "); - - foreach ($clients as &$c) { - $c['sumPoints'] = $c['sumPoints'] !== null ? (int)$c['sumPoints'] : null; - $c['startedAt'] = $c['startedAt'] !== null ? (int)$c['startedAt'] : null; - $c['completedAt'] = $c['completedAt'] !== null ? (int)$c['completedAt'] : null; - - $bindParams = array_merge([$c['clientCode']], $questionIDs); - $answerStmt->execute($bindParams); - $answers = $answerStmt->fetchAll(PDO::FETCH_ASSOC); - $answerMap = []; - foreach ($answers as $a) { - $answerMap[$a['questionID']] = [ - 'answerOptionID' => $a['answerOptionID'], - 'freeTextValue' => $a['freeTextValue'], - 'numericValue' => $a['numericValue'] !== null ? (float)$a['numericValue'] : null, - 'answeredAt' => $a['answeredAt'] !== null ? (int)$a['answeredAt'] : null, - ]; - } - $c['answers'] = $answerMap; - } - unset($c); -} else { - foreach ($clients as &$c) { - $c['sumPoints'] = $c['sumPoints'] !== null ? (int)$c['sumPoints'] : null; - $c['startedAt'] = $c['startedAt'] !== null ? (int)$c['startedAt'] : null; - $c['completedAt'] = $c['completedAt'] !== null ? (int)$c['completedAt'] : null; - $c['answers'] = (object)[]; - } - unset($c); -} - -qdb_discard($tmpDb, $lockFp); - -echo json_encode([ - "success" => true, - "questionnaire" => $questionnaire, - "questions" => $questions, - "clients" => $clients -]); diff --git a/api/translations.php b/api/translations.php deleted file mode 100644 index ebd39ff..0000000 --- a/api/translations.php +++ /dev/null @@ -1,291 +0,0 @@ - list all languages - if (isset($_GET['languages'])) { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - $rows = $pdo->query("SELECT languageCode, name FROM language ORDER BY languageCode")->fetchAll(PDO::FETCH_ASSOC); - qdb_discard($tmpDb, $lockFp); - echo json_encode(["success" => true, "languages" => $rows]); - break; - } - - // GET ?questionnaireID=X -> batch: all keys + all translations for a questionnaire - $qnID = $_GET['questionnaireID'] ?? ''; - if ($qnID) { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - - $languages = $pdo->query("SELECT languageCode, name FROM language ORDER BY languageCode")->fetchAll(PDO::FETCH_ASSOC); - - // Question keys + translations - $qStmt = $pdo->prepare(" - SELECT q.questionID, q.defaultText, q.configJson - FROM question q WHERE q.questionnaireID = :id ORDER BY q.orderIndex - "); - $qStmt->execute([':id' => $qnID]); - $dbQuestions = $qStmt->fetchAll(PDO::FETCH_ASSOC); - - $entries = []; - $stringKeys = []; - - foreach ($dbQuestions as $dbQ) { - $entries[] = [ - 'key' => $dbQ['defaultText'], - 'type' => 'question', - 'entityId' => $dbQ['questionID'], - ]; - - // Answer option keys - $aoStmt = $pdo->prepare(" - SELECT answerOptionID, defaultText FROM answer_option - WHERE questionID = :qid ORDER BY orderIndex - "); - $aoStmt->execute([':qid' => $dbQ['questionID']]); - foreach ($aoStmt->fetchAll(PDO::FETCH_ASSOC) as $ao) { - $entries[] = [ - 'key' => $ao['defaultText'], - 'type' => 'answer_option', - 'entityId' => $ao['answerOptionID'], - ]; - } - - // String keys from configJson - $config = json_decode($dbQ['configJson'] ?: '{}', true) ?: []; - foreach (['textKey','textKey1','textKey2','hint','hint1','hint2'] as $field) { - if (!empty($config[$field])) $stringKeys[$config[$field]] = true; - } - if (!empty($config['symptoms']) && is_array($config['symptoms'])) { - foreach ($config['symptoms'] as $s) $stringKeys[$s] = true; - } - } - - foreach (array_keys($stringKeys) as $sk) { - $entries[] = [ - 'key' => $sk, - 'type' => 'string', - 'entityId' => $sk, - ]; - } - - // Fetch all existing translations for these entities - $translations = []; - - // question translations - $qIds = array_filter(array_map(fn($e) => $e['type'] === 'question' ? $e['entityId'] : null, $entries)); - if ($qIds) { - $ph = implode(',', array_fill(0, count($qIds), '?')); - $stmt = $pdo->prepare("SELECT questionID AS entityId, languageCode, text FROM question_translation WHERE questionID IN ($ph)"); - $stmt->execute(array_values($qIds)); - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) { - $translations[$r['entityId']][$r['languageCode']] = $r['text']; - } - } - - // answer_option translations - $aoIds = array_filter(array_map(fn($e) => $e['type'] === 'answer_option' ? $e['entityId'] : null, $entries)); - if ($aoIds) { - $ph = implode(',', array_fill(0, count($aoIds), '?')); - $stmt = $pdo->prepare("SELECT answerOptionID AS entityId, languageCode, text FROM answer_option_translation WHERE answerOptionID IN ($ph)"); - $stmt->execute(array_values($aoIds)); - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) { - $translations[$r['entityId']][$r['languageCode']] = $r['text']; - } - } - - // string translations - $sKeys = array_filter(array_map(fn($e) => $e['type'] === 'string' ? $e['entityId'] : null, $entries)); - if ($sKeys) { - $ph = implode(',', array_fill(0, count($sKeys), '?')); - $stmt = $pdo->prepare("SELECT stringKey AS entityId, languageCode, text FROM string_translation WHERE stringKey IN ($ph)"); - $stmt->execute(array_values($sKeys)); - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) { - $translations[$r['entityId']][$r['languageCode']] = $r['text']; - } - } - - // Attach translations to entries - foreach ($entries as &$e) { - $e['translations'] = $translations[$e['entityId']] ?? (object)[]; - } - unset($e); - - qdb_discard($tmpDb, $lockFp); - echo json_encode([ - "success" => true, - "languages" => $languages, - "entries" => $entries, - ]); - break; - } - - // Original single-entity fetch - $type = $_GET['type'] ?? ''; - $id = $_GET['id'] ?? ''; - if (!in_array($type, $validTypes, true) || (!$id && $type !== 'string')) { - http_response_code(400); - echo json_encode(["error" => "type (question|answer_option|string) and id required"]); - exit; - } - - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - if ($type === 'question') { - $stmt = $pdo->prepare("SELECT languageCode, text FROM question_translation WHERE questionID = :id"); - $stmt->execute([':id' => $id]); - } elseif ($type === 'answer_option') { - $stmt = $pdo->prepare("SELECT languageCode, text FROM answer_option_translation WHERE answerOptionID = :id"); - $stmt->execute([':id' => $id]); - } else { - if ($id) { - $stmt = $pdo->prepare("SELECT stringKey, languageCode, text FROM string_translation WHERE stringKey = :id"); - $stmt->execute([':id' => $id]); - } else { - $stmt = $pdo->query("SELECT stringKey, languageCode, text FROM string_translation ORDER BY stringKey, languageCode"); - } - } - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - qdb_discard($tmpDb, $lockFp); - echo json_encode(["success" => true, "translations" => $rows]); - break; - -case 'PUT': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - $type = $body['type'] ?? ''; - - // Language management - if ($type === 'language') { - $lang = trim($body['languageCode'] ?? ''); - $name = trim($body['name'] ?? ''); - if (!$lang) { - http_response_code(400); - echo json_encode(["error" => "languageCode required"]); - exit; - } - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $pdo->prepare("INSERT INTO language (languageCode, name) VALUES (:lc, :n) - ON CONFLICT(languageCode) DO UPDATE SET name = excluded.name") - ->execute([':lc' => $lang, ':n' => $name]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - } - - $id = $body['id'] ?? ''; - $lang = trim($body['languageCode'] ?? ''); - $text = $body['text'] ?? ''; - if (!in_array($type, $validTypes, true) || !$id || !$lang) { - http_response_code(400); - echo json_encode(["error" => "type, id, and languageCode required"]); - exit; - } - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - if ($type === 'question') { - $pdo->prepare("INSERT INTO question_translation (questionID, languageCode, text) - VALUES (:id, :lang, :t) - ON CONFLICT(questionID, languageCode) DO UPDATE SET text = excluded.text") - ->execute([':id' => $id, ':lang' => $lang, ':t' => $text]); - } elseif ($type === 'answer_option') { - $pdo->prepare("INSERT INTO answer_option_translation (answerOptionID, languageCode, text) - VALUES (:id, :lang, :t) - ON CONFLICT(answerOptionID, languageCode) DO UPDATE SET text = excluded.text") - ->execute([':id' => $id, ':lang' => $lang, ':t' => $text]); - } else { - $pdo->prepare("INSERT INTO string_translation (stringKey, languageCode, text) - VALUES (:id, :lang, :t) - ON CONFLICT(stringKey, languageCode) DO UPDATE SET text = excluded.text") - ->execute([':id' => $id, ':lang' => $lang, ':t' => $text]); - } - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -case 'DELETE': - require_role(['admin', 'supervisor'], $tokenRec); - $body = json_decode(file_get_contents('php://input'), true); - $type = $body['type'] ?? ''; - - // Language deletion - if ($type === 'language') { - $lang = trim($body['languageCode'] ?? ''); - if (!$lang) { - http_response_code(400); - echo json_encode(["error" => "languageCode required"]); - exit; - } - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - $pdo->prepare("DELETE FROM language WHERE languageCode = :lc")->execute([':lc' => $lang]); - $pdo->prepare("DELETE FROM question_translation WHERE languageCode = :lc")->execute([':lc' => $lang]); - $pdo->prepare("DELETE FROM answer_option_translation WHERE languageCode = :lc")->execute([':lc' => $lang]); - $pdo->prepare("DELETE FROM string_translation WHERE languageCode = :lc")->execute([':lc' => $lang]); - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - } - - $id = $body['id'] ?? ''; - $lang = trim($body['languageCode'] ?? ''); - if (!in_array($type, $validTypes, true) || !$id || !$lang) { - http_response_code(400); - echo json_encode(["error" => "type, id, and languageCode required"]); - exit; - } - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - try { - if ($type === 'question') { - $pdo->prepare("DELETE FROM question_translation WHERE questionID = :id AND languageCode = :lang") - ->execute([':id' => $id, ':lang' => $lang]); - } elseif ($type === 'answer_option') { - $pdo->prepare("DELETE FROM answer_option_translation WHERE answerOptionID = :id AND languageCode = :lang") - ->execute([':id' => $id, ':lang' => $lang]); - } else { - $pdo->prepare("DELETE FROM string_translation WHERE stringKey = :id AND languageCode = :lang") - ->execute([':id' => $id, ':lang' => $lang]); - } - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true]); - } catch (Throwable $e) { - qdb_discard($tmpDb, $lockFp); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - } - break; - -default: - http_response_code(405); - echo json_encode(["error" => "Method not allowed"]); -} diff --git a/api/users.php b/api/users.php deleted file mode 100644 index 44465bf..0000000 --- a/api/users.php +++ /dev/null @@ -1,274 +0,0 @@ -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"]); diff --git a/assign_client.php b/assign_client.php deleted file mode 100644 index 9f51535..0000000 --- a/assign_client.php +++ /dev/null @@ -1,117 +0,0 @@ -query( - "SELECT c.coachID, c.username, c.supervisorID FROM coach c ORDER BY c.username" - )->fetchAll(PDO::FETCH_ASSOC); - } else { - $stmt = $pdo->prepare( - "SELECT c.coachID, c.username, c.supervisorID FROM coach c WHERE c.supervisorID = :sid ORDER BY c.username" - ); - $stmt->execute([':sid' => $entityID]); - $coaches = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - // Clients visible to this user - [$clause, $params] = rbac_client_filter($tokenRec); - $clientStmt = $pdo->prepare( - "SELECT cl.clientCode, cl.coachID, co.username AS coachUsername - FROM client cl - LEFT JOIN coach co ON co.coachID = cl.coachID - WHERE $clause - ORDER BY cl.clientCode" - ); - foreach ($params as $k => $v) $clientStmt->bindValue($k, $v); - $clientStmt->execute(); - $clients = $clientStmt->fetchAll(PDO::FETCH_ASSOC); - - $pdo = null; - qdb_discard($tmpDb, $lockFp); - - echo json_encode(["coaches" => $coaches, "clients" => $clients]); - } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - http_response_code(500); - echo json_encode(["error" => "Server error"]); - } - exit; -} - -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $raw = file_get_contents("php://input"); - $in = json_decode($raw, true) ?: []; - - $clientCodes = $in['clientCodes'] ?? []; - $coachID = trim($in['coachID'] ?? ''); - - if (empty($clientCodes) || $coachID === '') { - http_response_code(400); - echo json_encode(["error" => "clientCodes and coachID required"]); - exit; - } - if (!is_array($clientCodes)) { - $clientCodes = [$clientCodes]; - } - - try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - - // Verify coach exists and is visible - if ($role === 'supervisor') { - $chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :sid"); - $chk->execute([':cid' => $coachID, ':sid' => $entityID]); - } else { - $chk = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid"); - $chk->execute([':cid' => $coachID]); - } - if (!$chk->fetch()) { - qdb_discard($tmpDb, $lockFp); - http_response_code(400); - echo json_encode(["error" => "Coach not found or not authorized"]); - exit; - } - - // Only allow reassignment of clients the caller can already see - [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec); - $pdo->beginTransaction(); - $updStmt = $pdo->prepare( - "UPDATE client SET coachID = :cid WHERE clientCode = :cc AND ($rbacClause)" - ); - $count = 0; - foreach ($clientCodes as $cc) { - $cc = trim($cc); - if ($cc === '') continue; - $updStmt->execute(array_merge([':cid' => $coachID, ':cc' => $cc], $rbacParams)); - $count += $updStmt->rowCount(); - } - $pdo->commit(); - $pdo = null; - - qdb_save($tmpDb, $lockFp); - echo json_encode(["success" => true, "assigned" => $count]); - - } catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - http_response_code(500); - echo json_encode(["error" => "Server error"]); - } - exit; -} - -http_response_code(405); -echo json_encode(["error" => "Method not allowed"]); diff --git a/backup.php b/backup.php deleted file mode 100644 index cf203b2..0000000 --- a/backup.php +++ /dev/null @@ -1,42 +0,0 @@ - "No database to back up"]); - exit; -} - -if (!is_dir($backupDir)) { - if (!mkdir($backupDir, 0755, true) && !is_dir($backupDir)) { - http_response_code(500); - echo json_encode(["error" => "Could not create backups directory"]); - exit; - } -} - -$timestamp = date('Y-m-d_H-i-s'); -$backupFile = $backupDir . "/questionnaire_database.$timestamp"; - -if (!copy($dbPath, $backupFile)) { - http_response_code(500); - echo json_encode(["error" => "Backup copy failed"]); - exit; -} - -@chmod($backupFile, 0644); - -echo json_encode([ - "success" => true, - "message" => "Backup created", - "filename" => "questionnaire_database.$timestamp", -]); diff --git a/change_password.php b/change_password.php deleted file mode 100644 index b17c661..0000000 --- a/change_password.php +++ /dev/null @@ -1,113 +0,0 @@ - false, "message" => "Bearer token required"]); - exit; -} -$tokenRec = token_get_record($bearerToken); -if (!$tokenRec) { - http_response_code(403); - echo json_encode(["success" => false, "message" => "Invalid or expired token"]); - exit; -} - -if ($username === '' || $oldPassword === '' || $newPassword === '') { - http_response_code(400); - echo json_encode(["success" => false, "message" => "Missing fields"]); - exit; -} -if (strlen($newPassword) < 6) { - http_response_code(400); - echo json_encode(["success" => false, "message" => "New password too short (min 6 characters)."]); - exit; -} - -// Verify the token belongs to the user requesting the change -if (($tokenRec['userID'] ?? '') === '') { - http_response_code(403); - echo json_encode(["success" => false, "message" => "Token not associated with a user"]); - exit; -} - -try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - - $stmt = $pdo->prepare( - "SELECT userID, passwordHash, role, entityID FROM users WHERE username = :u" - ); - $stmt->execute([':u' => $username]); - $user = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!$user) { - qdb_discard($tmpDb, $lockFp); - http_response_code(401); - echo json_encode(["success" => false, "message" => "Invalid credentials."]); - exit; - } - - if ($user['userID'] !== $tokenRec['userID']) { - qdb_discard($tmpDb, $lockFp); - http_response_code(403); - echo json_encode(["success" => false, "message" => "Token does not match user"]); - exit; - } - - if (($user['role'] ?? '') === 'coach') { - qdb_discard($tmpDb, $lockFp); - http_response_code(403); - echo json_encode([ - "success" => false, - "message" => "Coach accounts can only sign in through the mobile app.", - ]); - exit; - } - - if (!password_verify($oldPassword, $user['passwordHash'])) { - qdb_discard($tmpDb, $lockFp); - http_response_code(401); - echo json_encode(["success" => false, "message" => "Old password incorrect."]); - exit; - } - - $newHash = password_hash($newPassword, PASSWORD_DEFAULT); - $upd = $pdo->prepare( - "UPDATE users SET passwordHash = :h, mustChangePassword = 0 WHERE userID = :uid" - ); - $upd->execute([':h' => $newHash, ':uid' => $user['userID']]); - $pdo = null; - - qdb_save($tmpDb, $lockFp); - - $token = bin2hex(random_bytes(32)); - token_add($token, 30 * 24 * 60 * 60, [ - 'role' => $user['role'], - 'entityID' => $user['entityID'], - 'userID' => $user['userID'], - ]); - - echo json_encode([ - "success" => true, - "message" => "Password changed.", - "token" => $token, - "user" => $username, - "role" => $user['role'], - ]); - -} catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - http_response_code(500); - echo json_encode(["success" => false, "message" => "Server error"]); -} diff --git a/checkDatabaseExists.php b/checkDatabaseExists.php deleted file mode 100755 index 67e4537..0000000 --- a/checkDatabaseExists.php +++ /dev/null @@ -1,10 +0,0 @@ - true]); -} else { - echo json_encode(["exists" => false]); -} diff --git a/db_download.php b/db_download.php deleted file mode 100644 index 617e2ab..0000000 --- a/db_download.php +++ /dev/null @@ -1,46 +0,0 @@ -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $fpdo->exec("PRAGMA foreign_keys = OFF;"); - [$clause, $params] = rbac_client_filter($tokenRec); - $delStmt = $fpdo->prepare("DELETE FROM client WHERE NOT ($clause)"); - foreach ($params as $k => $v) $delStmt->bindValue($k, $v); - $delStmt->execute(); - $fpdo->exec("DELETE FROM client_answer WHERE clientCode NOT IN (SELECT clientCode FROM client)"); - $fpdo->exec("DELETE FROM completed_questionnaire WHERE clientCode NOT IN (SELECT clientCode FROM client)"); - // Remove sensitive tables that non-admin users should never receive - foreach (['users', 'admin', 'supervisor', 'coach'] as $tbl) { - $fpdo->exec("DELETE FROM $tbl"); - } - $fpdo = null; - $plain = file_get_contents($tmpFilter); - } finally { - @unlink($tmpFilter); - } -} - -header('Content-Type: application/octet-stream'); -header('Content-Disposition: attachment; filename="questionnaire.sqlite"'); -header('Content-Length: ' . strlen($plain)); -echo $plain; diff --git a/db_view.php b/db_view.php deleted file mode 100644 index 4896167..0000000 --- a/db_view.php +++ /dev/null @@ -1,78 +0,0 @@ -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - $tables = []; - $rs = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"); - foreach ($rs as $row) $tables[] = $row['name']; - - // Tables that should not be shown (auth data) - $hidden = ['users']; - - echo ""; - echo "

Tables: " . htmlspecialchars(implode(', ', array_diff($tables, $hidden))) . "

"; - - [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec); - - foreach ($tables as $t) { - if (in_array($t, $hidden, true)) continue; - - echo "

" . htmlspecialchars($t) . "

"; - $t_esc = preg_replace('/[^A-Za-z0-9_]/', '', $t); - - $cols = []; - $cr = $pdo->query("PRAGMA table_info($t_esc)"); - foreach ($cr as $c) $cols[] = $c['name']; - - $clientScoped = in_array('clientCode', $cols, true); - - if ($clientScoped && ($tokenRec['role'] ?? '') !== 'admin') { - $sql = "SELECT t.* FROM $t_esc t JOIN client ON client.clientCode = t.clientCode WHERE $rbacClause"; - $stmt = $pdo->prepare($sql); - foreach ($rbacParams as $k => $v) $stmt->bindValue($k, $v); - $stmt->execute(); - } else { - $stmt = $pdo->query("SELECT * FROM $t_esc"); - } - - echo ""; - foreach ($cols as $c) echo ""; - echo ""; - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - echo ""; - foreach ($cols as $c) { - $val = $row[$c]; - if ($val === null) { echo ""; } - else { echo ""; } - } - echo ""; - } - echo "
" . htmlspecialchars($c) . "
NULL" . htmlspecialchars((string)$val) . "
"; - } -} catch (Throwable $e) { - error_log($e->getMessage()); - http_response_code(500); echo "Server error"; -} finally { - @unlink($tmp); -} diff --git a/db_view_ordered.php b/db_view_ordered.php deleted file mode 100644 index de77214..0000000 --- a/db_view_ordered.php +++ /dev/null @@ -1,156 +0,0 @@ -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec); - - $clientStmt = $pdo->prepare( - "SELECT client.clientCode FROM client WHERE $rbacClause ORDER BY client.clientCode" - ); - foreach ($rbacParams as $k => $v) $clientStmt->bindValue($k, $v); - $clientStmt->execute(); - $clients = $clientStmt->fetchAll(PDO::FETCH_COLUMN) ?: []; - - $questionnaireIds = $pdo->query("SELECT questionnaireID FROM questionnaire")->fetchAll(PDO::FETCH_COLUMN) ?: []; - $qidSet = array_fill_keys($questionnaireIds, true); - - $completed = []; - $cqStmt = $pdo->prepare( - "SELECT cq.clientCode, cq.questionnaireID, cq.status - FROM completed_questionnaire cq - JOIN client ON client.clientCode = cq.clientCode - WHERE $rbacClause" - ); - foreach ($rbacParams as $k => $v) $cqStmt->bindValue($k, $v); - $cqStmt->execute(); - while ($row = $cqStmt->fetch(PDO::FETCH_ASSOC)) { - $completed[$row['clientCode']][$row['questionnaireID']] = $row['status']; - } - - // client_answer uses questionID, which maps to old "questionId" format - $answers = []; - $ansStmt = $pdo->prepare( - "SELECT ca.clientCode, ca.questionID, ca.answerOptionID, ca.freeTextValue, ca.numericValue - FROM client_answer ca - JOIN client ON client.clientCode = ca.clientCode - WHERE $rbacClause" - ); - foreach ($rbacParams as $k => $v) $ansStmt->bindValue($k, $v); - $ansStmt->execute(); - while ($row = $ansStmt->fetch(PDO::FETCH_ASSOC)) { - $val = $row['freeTextValue'] ?? $row['answerOptionID'] ?? ''; - if ($val === '' && $row['numericValue'] !== null) { - $val = (string)$row['numericValue']; - } - $answers[$row['clientCode']][$row['questionID']] = $val; - } - - echo ''; - - echo ''; - echo ''; - echo ''; - foreach ($order as $id) echo ''; - echo ''; - - echo ''; - foreach ($order as $id) { - $key = qdb_header_question_key($id); - $lbl = $labels[$id] ?? ''; - $title = $lbl !== '' ? $lbl : $id; - echo ''; - } - echo ''; - - foreach ($clients as $client) { - $rowClass = stripos($client, 'DEV-CL-') === 0 ? ' row-test-data' : ''; - echo ''; - echo ''; - foreach ($order as $id) { - if ($id === 'client_code') { - $val = $client; - } elseif (isset($qidSet[$id]) && strpos($id, '-') === false) { - $status = $completed[$client][$id] ?? ''; - $val = ($status === 'done' || $status === 'Done') ? 'Done' : ($status !== '' ? $status : 'Not Done'); - } else { - $raw = $answers[$client][$id] ?? ''; - $val = (strlen(trim($raw)) > 0) ? t_val($id, $raw, $vals) : 'None'; - } - echo ''; - } - echo ''; - } - echo '
'.htmlspecialchars($id).'
'.htmlspecialchars($key).'
'.htmlspecialchars($val).'
'; - -} catch (Throwable $e) { - http_response_code(500); - error_log($e->getMessage()); - echo "Server error"; -} finally { - @unlink($tmp); -} diff --git a/dev-router.php b/dev-router.php deleted file mode 100644 index 4f90d75..0000000 --- a/dev-router.php +++ /dev/null @@ -1,38 +0,0 @@ -` (includes per-questionnaire `translations`). 7. Cache the client list, app translations, the questionnaire list, and questionnaire details (with embedded translations) locally for offline use. -8. When a questionnaire is completed, upload answers with `POST /api/app_questionnaires` (plaintext JSON over HTTPS; server stores answers in the database — not the app's local ciphertext). +8. When a questionnaire is completed, upload answers with `POST /api/app_questionnaires` using an **encrypted** JSON envelope (see above). There is currently no version or `updatedAt` field in the app fetch response, so the safest refresh strategy is to re-fetch translations, list, and details at login or app start when network is available. @@ -327,7 +327,31 @@ The Android app encrypts sensitive data **at rest on the device**: - **Assigned client list** cache and **session token** use the same encryption / EncryptedSharedPreferences respectively. - **Questionnaire definitions** and UI translations cached from this API are not encrypted (no user answers). -After a successful `POST /api/app_questionnaires`, answer data is readable on the **server** (normal database storage, RBAC-protected). The upload body is **decrypted plaintext** (AES-GCM applies only to the on-device Room database). The app keeps encrypted local copies so coaches can review, edit, and re-upload work offline. +Sensitive mobile traffic uses **two layers**: + +1. **HTTPS** (TLS) for transport. +2. **Application payload encryption** for patient-related fields: AES-256-CBC with a per-session key derived from the Bearer token via HKDF-SHA256 (`info` = `qdb-aes`), matching the legacy QDB mobile crypto helpers. + +Encrypted wire format (request or response `data`): + +```json +{ + "encrypted": true, + "payload": "" +} +``` + +Endpoints using encrypted payloads: + +| Endpoint | Direction | +|----------|-----------| +| `POST /api/auth/login` | Response: `clientsPayload` (when coach has clients) | +| `GET /api/app_questionnaires?clients=1` | Response: entire `data` object encrypted | +| `POST /api/app_questionnaires` | Request body must be encrypted | + +Plain JSON (no app-layer encryption): `GET ?translations=1`, questionnaire list, questionnaire detail. + +The server decrypts uploads before writing to the database. The web dashboard reads **server-side** plaintext (RBAC). The Android app also keeps **AES-GCM at rest** on device via Android Keystore (separate from wire encryption). Re-uploading the same `clientCode` + `questionnaireID` updates server rows in place (see upload behavior below). This matches upcoming editable flows on web and mobile. diff --git a/downloadFull.php b/downloadFull.php deleted file mode 100755 index e28b7d9..0000000 --- a/downloadFull.php +++ /dev/null @@ -1,51 +0,0 @@ -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $fpdo->exec("PRAGMA foreign_keys = OFF;"); - [$clause, $params] = rbac_client_filter($tokenRec); - $delStmt = $fpdo->prepare("DELETE FROM client WHERE NOT ($clause)"); - foreach ($params as $k => $v) $delStmt->bindValue($k, $v); - $delStmt->execute(); - $fpdo->exec("DELETE FROM client_answer WHERE clientCode NOT IN (SELECT clientCode FROM client)"); - $fpdo->exec("DELETE FROM completed_questionnaire WHERE clientCode NOT IN (SELECT clientCode FROM client)"); - // Remove sensitive tables that non-admin users should never receive - foreach (['users', 'admin', 'supervisor', 'coach'] as $tbl) { - $fpdo->exec("DELETE FROM $tbl"); - } - $fpdo = null; - $plain = file_get_contents($tmpFilter); - } finally { - @unlink($tmpFilter); - } -} - -$sessionKey = hkdf_session_key_from_token($token); -$out = aes256_cbc_encrypt_bytes($plain, $sessionKey); - -header('Content-Type: application/octet-stream'); -header('Content-Disposition: attachment; filename="questionnaire_database"'); -header('Content-Length: ' . strlen($out)); -echo $out; diff --git a/handlers/app_questionnaires.php b/handlers/app_questionnaires.php index 4219ade..665ba68 100644 --- a/handlers/app_questionnaires.php +++ b/handlers/app_questionnaires.php @@ -7,7 +7,7 @@ * GET ?clients=1 -> list of clients assigned to the authenticated coach * POST -> submit interview answers for a client (coach / supervisor / admin). * Re-posting the same clientCode + questionnaireID updates answers in place (re-edit). - * Mobile clients encrypt answers at rest on device; POST body is plaintext JSON over HTTPS. + * Sensitive POST/GET ?clients=1 bodies use encrypted payloads (HKDF from Bearer token). */ // Public app UI catalog for the login screen (no token, no PII). @@ -23,7 +23,7 @@ $tokenRec = require_valid_token(); if ($method === 'POST') { require_role(['admin', 'supervisor', 'coach'], $tokenRec); - $body = read_json_body(); + $body = read_encrypted_json_body($tokenRec['_token']); require_fields($body, ['questionnaireID', 'clientCode', 'answers']); $qnID = trim($body['questionnaireID']); @@ -259,7 +259,7 @@ if ($fetchClients) { }, $clientCodes); qdb_discard($tmpDb, $lockFp); - json_success(['coachID' => $coachID, 'clients' => $clients]); + json_success_sensitive(['coachID' => $coachID, 'clients' => $clients], $tokenRec['_token']); } if ($qnID) { diff --git a/handlers/auth.php b/handlers/auth.php index 054d933..6399d8e 100644 --- a/handlers/auth.php +++ b/handlers/auth.php @@ -67,12 +67,18 @@ case 'auth/login': 'entityID' => $user['entityID'], 'userID' => $user['userID'], ]); - json_success([ - 'token' => $token, - 'user' => $username, - 'role' => $user['role'], - 'clients' => $assignedClients, - ]); + $loginData = [ + 'token' => $token, + 'user' => $username, + 'role' => $user['role'], + ]; + if ($assignedClients !== []) { + $loginData['clientsPayload'] = qdb_sensitive_envelope( + json_encode(['clients' => $assignedClients], JSON_UNESCAPED_UNICODE), + $token + ); + } + json_success($loginData); } catch (Throwable $e) { if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); error_log($e->getMessage()); diff --git a/handlers/download.php b/handlers/download.php deleted file mode 100644 index 9e550bb..0000000 --- a/handlers/download.php +++ /dev/null @@ -1,73 +0,0 @@ -getMessage()); - json_error('SERVER_ERROR', 'Decryption failed', 500); -} - -$role = $tokenRec['role'] ?? ''; -if ($role !== 'admin') { - $tmpFilter = tempnam(sys_get_temp_dir(), 'qdb_filt_'); - file_put_contents($tmpFilter, $plain); - try { - $fpdo = new PDO("sqlite:$tmpFilter"); - $fpdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $fpdo->exec("PRAGMA foreign_keys = OFF;"); - - [$clause, $params] = rbac_client_filter($tokenRec); - $delStmt = $fpdo->prepare("DELETE FROM client WHERE NOT ($clause)"); - foreach ($params as $k => $v) $delStmt->bindValue($k, $v); - $delStmt->execute(); - - $fpdo->exec("DELETE FROM client_answer WHERE clientCode NOT IN (SELECT clientCode FROM client)"); - $fpdo->exec("DELETE FROM completed_questionnaire WHERE clientCode NOT IN (SELECT clientCode FROM client)"); - - foreach (['users', 'admin', 'supervisor', 'coach'] as $tbl) { - $fpdo->exec("DELETE FROM $tbl"); - } - - $fpdo = null; - $plain = file_get_contents($tmpFilter); - } finally { - @unlink($tmpFilter); - } -} - -$type = strtolower(trim($_GET['type'] ?? 'encrypted')); - -if ($type === 'plain') { - header('Content-Type: application/octet-stream'); - header('Content-Disposition: attachment; filename="questionnaire_database.sqlite"'); - header('Content-Length: ' . strlen($plain)); - echo $plain; - exit; -} - -$sessionKey = hkdf_session_key_from_token($token); -$out = aes256_cbc_encrypt_bytes($plain, $sessionKey); - -header('Content-Type: application/octet-stream'); -header('Content-Disposition: attachment; filename="questionnaire_database"'); -header('Content-Length: ' . strlen($out)); -echo $out; -exit; diff --git a/header_order.json b/header_order.json deleted file mode 100644 index 31c0c8e..0000000 --- a/header_order.json +++ /dev/null @@ -1,106 +0,0 @@ -[ - "client_code", - "questionnaire_1_demographic_information", - "questionnaire_1_demographic_information-coach_code", - "questionnaire_1_demographic_information-consent_instruction", - "questionnaire_1_demographic_information-no_consent_entered", - "questionnaire_1_demographic_information-accommodation", - "questionnaire_1_demographic_information-other_accommodation", - "questionnaire_1_demographic_information-client_code_entry_question", - "questionnaire_1_demographic_information-age", - "questionnaire_1_demographic_information-gender", - "questionnaire_1_demographic_information-country_of_origin", - "questionnaire_1_demographic_information-departure_country", - "questionnaire_1_demographic_information-since_in_germany", - "questionnaire_1_demographic_information-living_situation", - "questionnaire_1_demographic_information-number_family_members", - "questionnaire_1_demographic_information-languages_spoken", - "questionnaire_1_demographic_information-german_skills", - "questionnaire_1_demographic_information-school_years_total", - "questionnaire_1_demographic_information-school_years_origin", - "questionnaire_1_demographic_information-school_years_transit", - "questionnaire_1_demographic_information-school_years_germany", - "questionnaire_1_demographic_information-vocational_training", - "questionnaire_1_demographic_information-provisional_accommodation_since", - "questionnaire_2_rhs", - "questionnaire_2_rhs-coach_code", - "questionnaire_2_rhs-glass_explanation", - "questionnaire_2_rhs-q1_symptom", - "questionnaire_2_rhs-q2_symptom", - "questionnaire_2_rhs-q3_symptom", - "questionnaire_2_rhs-q4_symptom", - "questionnaire_2_rhs-q5_symptom", - "questionnaire_2_rhs-q6_symptom", - "questionnaire_2_rhs-q7_symptom", - "questionnaire_2_rhs-q8_symptom", - "questionnaire_2_rhs-q9_symptom", - "questionnaire_2_rhs-q10_reexperience_trauma", - "questionnaire_2_rhs-q11_physical_reaction", - "questionnaire_2_rhs-q12_emotional_numbness", - "questionnaire_2_rhs-q13_easily_startled", - "questionnaire_2_rhs-q14_intro", - "questionnaire_2_rhs-pain_rating_instruction", - "questionnaire_2_rhs-violence_question_1", - "questionnaire_2_rhs-times_happend", - "questionnaire_2_rhs-age_at_incident", - "questionnaire_2_rhs-conflict_since_arrival", - "questionnaire_2_rhs-times_happend2", - "questionnaire_2_rhs-age_at_incident2", - "questionnaire_2_rhs-asylum_procedure_since", - "questionnaire_3_integration_index", - "questionnaire_3_integration_index-coach_code", - "questionnaire_3_integration_index-feeling_connected_to_germany_question", - "questionnaire_3_integration_index-understanding_political_issues", - "questionnaire_3_integration_index-unexpected_expense_question", - "questionnaire_3_integration_index-dining_with_germans_question", - "questionnaire_3_integration_index-reading_german_articles_question", - "questionnaire_3_integration_index-visiting_doctor_question", - "questionnaire_3_integration_index-feeling_as_outsider_question", - "questionnaire_3_integration_index-recent_occupation_question", - "questionnaire_3_integration_index-discussing_politics_question", - "questionnaire_3_integration_index-contact_with_germans_question", - "questionnaire_3_integration_index-speaking_german_opinion_question", - "questionnaire_3_integration_index-job_search_question", - "questionnaire_4_consultation_results", - "questionnaire_4_consultation_results-coach_code", - "questionnaire_4_consultation_results-date_consultation_health_interview_result", - "questionnaire_4_consultation_results-consultation_decision", - "questionnaire_4_consultation_results-consent_conversation_in_6_months", - "questionnaire_4_consultation_results-participation_in_coaching", - "questionnaire_4_consultation_results-consent_coaching_given", - "questionnaire_4_consultation_results-consent_conversation_in_6_months", - "questionnaire_4_consultation_results-decision_after_reflection_period", - "questionnaire_4_consultation_results-professional_referral", - "questionnaire_4_consultation_results-confidentiality_agreement", - "questionnaire_4_consultation_results-health_insurance_card", - "questionnaire_4_consultation_results-consent_conversation_in_6_months", - "questionnaire_5_final_interview", - "questionnaire_5_final_interview-coach_code", - "questionnaire_5_final_interview-consent_followup_6_months", - "questionnaire_5_final_interview-date_final_interview", - "questionnaire_5_final_interview-amount_nat_appointments", - "questionnaire_5_final_interview-amount_session_flowers", - "questionnaire_5_final_interview-amount_session_stones", - "questionnaire_5_final_interview-termination_nat_coaching", - "questionnaire_5_final_interview-client_canceled_NAT", - "questionnaire_6_follow_up_survey", - "questionnaire_6_follow_up_survey-coach_code", - "questionnaire_6_follow_up_survey-follow_up", - "questionnaire_6_follow_up_survey-special_burden_question", - "questionnaire_6_follow_up_survey-glass_explanation", - "questionnaire_6_follow_up_survey-how_strong_past_month", - "questionnaire_6_follow_up_survey-q14_intro", - "questionnaire_6_follow_up_survey-pain_rating_instruction", - "questionnaire_6_follow_up_survey-feeling_connected_to_germany_question", - "questionnaire_6_follow_up_survey-understanding_political_issues", - "questionnaire_6_follow_up_survey-unexpected_expense_question", - "questionnaire_6_follow_up_survey-dining_with_germans_question", - "questionnaire_6_follow_up_survey-reading_german_articles_question", - "questionnaire_6_follow_up_survey-visiting_doctor_question", - "questionnaire_6_follow_up_survey-feeling_as_outsider_question", - "questionnaire_6_follow_up_survey-recent_occupation_question", - "questionnaire_6_follow_up_survey-discussing_politics_question", - "questionnaire_6_follow_up_survey-contact_with_germans_question", - "questionnaire_6_follow_up_survey-speaking_german_opinion_question", - "questionnaire_6_follow_up_survey-job_search_question" -] diff --git a/import_questionnaires.php b/import_questionnaires.php deleted file mode 100644 index 32bb4b3..0000000 --- a/import_questionnaires.php +++ /dev/null @@ -1,195 +0,0 @@ - $err]); - exit; -} - -[$pdo, $tmpDb, $lockFp] = qdb_open(true); - -try { - $pdo->beginTransaction(); - - foreach ($orderData as $orderIdx => $entry) { - $filename = $entry['file']; - $showPoints = (int)($entry['showPoints'] ?? 0); - $condition = json_encode($entry['condition'] ?? new stdClass()); - - $jsonPath = $assetsDir . '/' . $filename; - $qData = json_decode(file_get_contents($jsonPath), true); - if (!$qData) { - throw new Exception("Could not parse $filename"); - } - - $questionnaireID = $qData['meta']['id']; - $name = str_replace('_', ' ', preg_replace('/^questionnaire_\d+_/', '', $questionnaireID)); - $name = ucwords($name); - - out("Importing: $questionnaireID ($name)", $isCli); - - $pdo->prepare("INSERT OR REPLACE INTO questionnaire - (questionnaireID, name, version, state, orderIndex, showPoints, conditionJson) - VALUES (:id, :n, :v, :s, :o, :sp, :cj)") - ->execute([ - ':id' => $questionnaireID, - ':n' => $name, - ':v' => '1.0', - ':s' => 'active', - ':o' => $orderIdx, - ':sp' => $showPoints, - ':cj' => $condition, - ]); - - $questions = $qData['questions'] ?? []; - foreach ($questions as $qIdx => $q) { - $qLocalId = $q['id']; - $layout = $q['layout']; - $qKey = $q['question'] ?? ''; - - $questionID = $questionnaireID . '__' . $qLocalId; - - $config = []; - - if (isset($q['textKey'])) $config['textKey'] = $q['textKey']; - if (isset($q['textKey1'])) $config['textKey1'] = $q['textKey1']; - if (isset($q['textKey2'])) $config['textKey2'] = $q['textKey2']; - if (isset($q['hint'])) $config['hint'] = $q['hint']; - if (isset($q['hint1'])) $config['hint1'] = $q['hint1']; - if (isset($q['hint2'])) $config['hint2'] = $q['hint2']; - if (isset($q['symptoms'])) $config['symptoms'] = $q['symptoms']; - if (isset($q['scaleType'])) $config['scaleType'] = $q['scaleType']; - if (isset($q['range'])) $config['range'] = $q['range']; - if (isset($q['constraints'])) $config['constraints'] = $q['constraints']; - if (isset($q['precision'])) $config['precision'] = $q['precision']; - if (isset($q['minSelection'])) $config['minSelection'] = $q['minSelection']; - if (isset($q['maxLength'])) $config['maxLength'] = (int)$q['maxLength']; - if (isset($q['nextQuestionId'])) $config['nextQuestionId'] = $q['nextQuestionId']; - if (isset($q['otherNextQuestionId'])) $config['otherNextQuestionId'] = $q['otherNextQuestionId']; - if (isset($q['otherOptionKey'])) $config['otherOptionKey'] = $q['otherOptionKey']; - if (isset($q['config']) && is_array($q['config'])) { - foreach (['nextQuestionId', 'otherNextQuestionId', 'otherOptionKey', 'hint', 'maxLength', 'textKey', 'precision', 'scaleType', 'noteBefore', 'noteAfter', 'step', 'unitLabel'] as $ck) { - if (isset($q['config'][$ck])) { - $config[$ck] = $q['config'][$ck]; - } - } - } - - if ($layout === 'string_spinner' && isset($q['options']) && is_array($q['options']) - && isset($q['options'][0]) && is_string($q['options'][0])) { - $config['options'] = $q['options']; - } - - if ($layout === 'value_spinner' && isset($q['options']) && is_array($q['options'])) { - $valueOptions = []; - foreach ($q['options'] as $vo) { - if (is_array($vo) && isset($vo['value'])) { - $valueOptions[] = $vo; - } - } - if ($valueOptions) { - $config['valueOptions'] = $valueOptions; - } - } - - if ($qKey !== '') { - $config = qdb_normalize_question_config($config, $qKey); - } - $configJson = !empty($config) ? json_encode($config) : '{}'; - $germanText = $qKey; - - $pdo->prepare("INSERT OR REPLACE INTO question - (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson) - VALUES (:id, :qnid, :dt, :ty, :oi, :ir, :cj)") - ->execute([ - ':id' => $questionID, - ':qnid' => $questionnaireID, - ':dt' => $germanText, - ':ty' => $layout, - ':oi' => $qIdx, - ':ir' => 0, - ':cj' => $configJson, - ]); - if ($qKey !== '') { - qdb_upsert_source_translation($pdo, 'question', $questionID, $germanText); - qdb_sync_question_note_strings($pdo, $qKey, $config); - } - - if (isset($q['options']) && is_array($q['options'])) { - $hasObjects = isset($q['options'][0]) && is_array($q['options'][0]); - if ($hasObjects) { - $pointsMap = $q['pointsMap'] ?? []; - foreach ($q['options'] as $optIdx => $opt) { - $optKey = $opt['key'] ?? ''; - if (!$optKey) continue; - - $nextQId = ''; - if (isset($opt['nextQuestionId'])) { - $nextQId = $opt['nextQuestionId']; - } - $pts = (int)($pointsMap[$optKey] ?? 0); - $answerOptionID = $questionID . '__opt_' . $optIdx; - - $pdo->prepare("INSERT OR REPLACE INTO answer_option - (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId) - VALUES (:id, :qid, :dt, :p, :oi, :nq)") - ->execute([ - ':id' => $answerOptionID, - ':qid' => $questionID, - ':dt' => $optKey, - ':p' => $pts, - ':oi' => $optIdx, - ':nq' => $nextQId, - ]); - } - } - } - } - - out(" -> " . count($questions) . " questions imported", $isCli); - } - - $pdo->commit(); - qdb_save($tmpDb, $lockFp); - - if ($isCli) { - echo "\nImport complete.\n"; - } else { - echo json_encode(["success" => true, "message" => "Import complete"]); - } - -} catch (Throwable $e) { - if ($pdo->inTransaction()) $pdo->rollBack(); - qdb_discard($tmpDb, $lockFp); - if ($isCli) { - die("ERROR: " . $e->getMessage() . "\n"); - } - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); -} diff --git a/index.html b/index.html deleted file mode 100644 index dc81c81..0000000 --- a/index.html +++ /dev/null @@ -1,975 +0,0 @@ - - - - - Questionnaire Database - - - - - -

Questionnaire Database

- - -
-

Login

-
-
- - -
-
- - -
- -
-

-
- - - - - - - - - - - - - - - - - - - diff --git a/lib/encrypted_payload.php b/lib/encrypted_payload.php new file mode 100644 index 0000000..dd9d3e9 --- /dev/null +++ b/lib/encrypted_payload.php @@ -0,0 +1,25 @@ + true, + 'payload' => base64_encode(aes256_cbc_encrypt_bytes($plainJson, $key)), + ]; +} + +function qdb_decrypt_sensitive_envelope(array $envelope, string $tokenHex): string { + if (empty($envelope['encrypted']) || !isset($envelope['payload']) || !is_string($envelope['payload'])) { + throw new InvalidArgumentException('Invalid encrypted envelope'); + } + $raw = base64_decode($envelope['payload'], true); + if ($raw === false) { + throw new InvalidArgumentException('Invalid base64 payload'); + } + $key = hkdf_session_key_from_token($tokenHex); + return aes256_cbc_decrypt_bytes($raw, $key); +} diff --git a/lib/repositories/ClientRepo.php b/lib/repositories/ClientRepo.php deleted file mode 100644 index cc44d9f..0000000 --- a/lib/repositories/ClientRepo.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ - public static function listCoaches(PDO $pdo, string $callerRole, string $callerEntityID): array - { - if ($callerRole === 'admin') { - return $pdo->query(" - SELECT c.coachID, c.username, c.supervisorID, sv.username AS supervisorUsername - FROM coach c - LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID - ORDER BY sv.username, c.username - ")->fetchAll(PDO::FETCH_ASSOC); - } - - $stmt = $pdo->prepare(" - SELECT c.coachID, c.username, c.supervisorID, sv.username AS supervisorUsername - FROM coach c - LEFT JOIN supervisor sv ON sv.supervisorID = c.supervisorID - WHERE c.supervisorID = :sid - ORDER BY c.username - "); - $stmt->execute([':sid' => $callerEntityID]); - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * List clients visible to the caller (RBAC-filtered). - * - * @return list - */ - public static function listClients(PDO $pdo, array $tokenRecord): array - { - [$clause, $params] = rbac_client_filter($tokenRecord); - - $stmt = $pdo->prepare(" - SELECT cl.clientCode, cl.coachID, co.username AS coachUsername - FROM client cl - LEFT JOIN coach co ON co.coachID = cl.coachID - WHERE $clause - ORDER BY cl.coachID, cl.clientCode - "); - foreach ($params as $k => $v) { - $stmt->bindValue($k, $v); - } - $stmt->execute(); - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * Re-assign clients to a coach, respecting RBAC. - * - * @param list $clientCodes - * @return int Number of rows actually updated. - */ - public static function assignToCoach(PDO $pdo, array $clientCodes, string $coachID, array $tokenRecord): int - { - [$rbacClause, $rbacParams] = rbac_client_filter($tokenRecord); - - $stmt = $pdo->prepare( - "UPDATE client SET coachID = :cid WHERE clientCode = :cc AND ($rbacClause)" - ); - $count = 0; - - foreach ($clientCodes as $cc) { - $cc = trim($cc); - if ($cc === '') { - continue; - } - $stmt->execute(array_merge([':cid' => $coachID, ':cc' => $cc], $rbacParams)); - $count += $stmt->rowCount(); - } - - return $count; - } - - /** - * Check that a coach exists and is visible to the caller. - */ - public static function coachExists(PDO $pdo, string $coachID, string $callerRole, string $callerEntityID): bool - { - if ($callerRole === 'supervisor') { - $stmt = $pdo->prepare( - "SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :sid" - ); - $stmt->execute([':cid' => $coachID, ':sid' => $callerEntityID]); - } else { - $stmt = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid"); - $stmt->execute([':cid' => $coachID]); - } - - return (bool) $stmt->fetch(); - } -} diff --git a/lib/repositories/QuestionRepo.php b/lib/repositories/QuestionRepo.php deleted file mode 100644 index fa062c2..0000000 --- a/lib/repositories/QuestionRepo.php +++ /dev/null @@ -1,146 +0,0 @@ -prepare(" - SELECT questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson - FROM question WHERE questionnaireID = :qid ORDER BY orderIndex - "); - $stmt->execute([':qid' => $questionnaireID]); - $questions = $stmt->fetchAll(PDO::FETCH_ASSOC); - - foreach ($questions as &$q) { - $q['isRequired'] = (int) $q['isRequired']; - $q['orderIndex'] = (int) $q['orderIndex']; - $q['configJson'] = $q['configJson'] ?: '{}'; - - $ao = $pdo->prepare(" - SELECT answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId - FROM answer_option WHERE questionID = :qid ORDER BY orderIndex - "); - $ao->execute([':qid' => $q['questionID']]); - $q['answerOptions'] = $ao->fetchAll(PDO::FETCH_ASSOC); - foreach ($q['answerOptions'] as &$opt) { - $opt['points'] = (int) $opt['points']; - $opt['orderIndex'] = (int) $opt['orderIndex']; - } - unset($opt); - - $tr = $pdo->prepare( - 'SELECT languageCode, text FROM question_translation WHERE questionID = :qid' - ); - $tr->execute([':qid' => $q['questionID']]); - $q['translations'] = $tr->fetchAll(PDO::FETCH_ASSOC); - } - unset($q); - - return $questions; - } - - public static function findById(PDO $pdo, string $questionID): ?array - { - $stmt = $pdo->prepare('SELECT * FROM question WHERE questionID = :id'); - $stmt->execute([':id' => $questionID]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - return $row === false ? null : $row; - } - - public static function create( - PDO $pdo, - string $id, - string $questionnaireID, - string $defaultText, - string $type, - int $orderIndex, - int $isRequired, - string $configJson - ): void { - $pdo->prepare( - 'INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson) - VALUES (:id, :qid, :t, :ty, :o, :r, :cj)' - )->execute([ - ':id' => $id, - ':qid' => $questionnaireID, - ':t' => $defaultText, - ':ty' => $type, - ':o' => $orderIndex, - ':r' => $isRequired, - ':cj' => $configJson, - ]); - } - - public static function update( - PDO $pdo, - string $id, - string $defaultText, - string $type, - int $orderIndex, - int $isRequired, - string $configJson - ): void { - $pdo->prepare( - 'UPDATE question SET defaultText = :t, type = :ty, orderIndex = :o, isRequired = :r, configJson = :cj - WHERE questionID = :id' - )->execute([ - ':t' => $defaultText, - ':ty' => $type, - ':o' => $orderIndex, - ':r' => $isRequired, - ':cj' => $configJson, - ':id' => $id, - ]); - } - - public static function delete(PDO $pdo, string $id): void - { - $pdo->exec('PRAGMA foreign_keys = OFF;'); - $pdo->beginTransaction(); - try { - $aoIds = $pdo->prepare('SELECT answerOptionID FROM answer_option WHERE questionID = :id'); - $aoIds->execute([':id' => $id]); - foreach ($aoIds->fetchAll(PDO::FETCH_COLUMN) as $aoid) { - $pdo->prepare('DELETE FROM answer_option_translation WHERE answerOptionID = :id') - ->execute([':id' => $aoid]); - } - $pdo->prepare('DELETE FROM answer_option WHERE questionID = :id')->execute([':id' => $id]); - $pdo->prepare('DELETE FROM question_translation WHERE questionID = :id')->execute([':id' => $id]); - $pdo->prepare('DELETE FROM client_answer WHERE questionID = :id')->execute([':id' => $id]); - $pdo->prepare('DELETE FROM question WHERE questionID = :id')->execute([':id' => $id]); - $pdo->commit(); - } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - throw $e; - } finally { - $pdo->exec('PRAGMA foreign_keys = ON;'); - } - } - - public static function reorder(PDO $pdo, string $questionnaireID, array $orderedIds): void - { - $stmt = $pdo->prepare( - 'UPDATE question SET orderIndex = :o WHERE questionID = :id AND questionnaireID = :qid' - ); - foreach ($orderedIds as $idx => $questionId) { - $stmt->execute([ - ':o' => (int) $idx, - ':id' => $questionId, - ':qid' => $questionnaireID, - ]); - } - } - - public static function nextOrderIndex(PDO $pdo, string $questionnaireID): int - { - $max = $pdo->prepare( - 'SELECT COALESCE(MAX(orderIndex), 0) + 1 FROM question WHERE questionnaireID = :id' - ); - $max->execute([':id' => $questionnaireID]); - - return (int) $max->fetchColumn(); - } -} diff --git a/lib/repositories/QuestionnaireRepo.php b/lib/repositories/QuestionnaireRepo.php deleted file mode 100644 index 5b8d381..0000000 --- a/lib/repositories/QuestionnaireRepo.php +++ /dev/null @@ -1,178 +0,0 @@ - - */ - public static function listAll(PDO $pdo): array - { - $rows = $pdo->query(" - SELECT q.questionnaireID, q.name, q.version, q.state, - q.orderIndex, q.showPoints, q.conditionJson, - COUNT(qu.questionID) AS questionCount - FROM questionnaire q - LEFT JOIN question qu ON qu.questionnaireID = q.questionnaireID - GROUP BY q.questionnaireID - ORDER BY q.orderIndex, q.name - ")->fetchAll(PDO::FETCH_ASSOC); - - foreach ($rows as &$r) { - $r['orderIndex'] = (int) $r['orderIndex']; - $r['showPoints'] = (int) $r['showPoints']; - $r['conditionJson'] = $r['conditionJson'] ?: '{}'; - $r['questionCount'] = (int) $r['questionCount']; - } - unset($r); - - return $rows; - } - - /** - * @return list - */ - public static function listActive(PDO $pdo): array - { - $rows = $pdo->query(" - SELECT questionnaireID, name, showPoints, conditionJson - FROM questionnaire - WHERE state = 'active' - ORDER BY orderIndex - ")->fetchAll(PDO::FETCH_ASSOC); - - $list = []; - foreach ($rows as $r) { - $list[] = [ - 'id' => $r['questionnaireID'], - 'name' => $r['name'], - 'showPoints' => (bool) (int) $r['showPoints'], - 'condition' => json_decode($r['conditionJson'], true) ?: new stdClass(), - ]; - } - - return $list; - } - - /** - * @return array|null - */ - public static function findById(PDO $pdo, string $id): ?array - { - $stmt = $pdo->prepare('SELECT * FROM questionnaire WHERE questionnaireID = :id'); - $stmt->execute([':id' => $id]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - return $row ?: null; - } - - public static function create( - PDO $pdo, - string $id, - string $name, - string $version, - string $state, - int $orderIndex, - int $showPoints, - string $conditionJson - ): void { - $stmt = $pdo->prepare(' - INSERT INTO questionnaire (questionnaireID, name, version, state, orderIndex, showPoints, conditionJson) - VALUES (:id, :n, :v, :s, :o, :sp, :cj) - '); - $stmt->execute([ - ':id' => $id, - ':n' => $name, - ':v' => $version, - ':s' => $state, - ':o' => $orderIndex, - ':sp' => $showPoints, - ':cj' => $conditionJson, - ]); - } - - public static function update( - PDO $pdo, - string $id, - string $name, - string $version, - string $state, - int $orderIndex, - int $showPoints, - string $conditionJson - ): void { - $stmt = $pdo->prepare(' - UPDATE questionnaire - SET name = :n, version = :v, state = :s, - orderIndex = :o, showPoints = :sp, conditionJson = :cj - WHERE questionnaireID = :id - '); - $stmt->execute([ - ':n' => $name, - ':v' => $version, - ':s' => $state, - ':o' => $orderIndex, - ':sp' => $showPoints, - ':cj' => $conditionJson, - ':id' => $id, - ]); - } - - public static function delete(PDO $pdo, string $id): void - { - $pdo->exec('PRAGMA foreign_keys = OFF;'); - try { - $pdo->beginTransaction(); - - $qIds = $pdo->prepare('SELECT questionID FROM question WHERE questionnaireID = :id'); - $qIds->execute([':id' => $id]); - $questionIDs = $qIds->fetchAll(PDO::FETCH_COLUMN); - - foreach ($questionIDs as $qid) { - $aoIds = $pdo->prepare('SELECT answerOptionID FROM answer_option WHERE questionID = :qid'); - $aoIds->execute([':qid' => $qid]); - $optionIDs = $aoIds->fetchAll(PDO::FETCH_COLUMN); - - foreach ($optionIDs as $aoid) { - $delAot = $pdo->prepare('DELETE FROM answer_option_translation WHERE answerOptionID = :id'); - $delAot->execute([':id' => $aoid]); - } - - $pdo->prepare('DELETE FROM answer_option WHERE questionID = :qid')->execute([':qid' => $qid]); - $pdo->prepare('DELETE FROM question_translation WHERE questionID = :qid')->execute([':qid' => $qid]); - $pdo->prepare('DELETE FROM client_answer WHERE questionID = :qid')->execute([':qid' => $qid]); - } - - $pdo->prepare('DELETE FROM question WHERE questionnaireID = :id')->execute([':id' => $id]); - $pdo->prepare('DELETE FROM completed_questionnaire WHERE questionnaireID = :id')->execute([':id' => $id]); - $pdo->prepare('DELETE FROM questionnaire WHERE questionnaireID = :id')->execute([':id' => $id]); - - $pdo->commit(); - } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - throw $e; - } finally { - $pdo->exec('PRAGMA foreign_keys = ON;'); - } - } - - public static function nextOrderIndex(PDO $pdo): int - { - return (int) $pdo->query('SELECT COALESCE(MAX(orderIndex),0)+1 FROM questionnaire')->fetchColumn(); - } -} diff --git a/lib/repositories/ResultRepo.php b/lib/repositories/ResultRepo.php deleted file mode 100644 index 7d32100..0000000 --- a/lib/repositories/ResultRepo.php +++ /dev/null @@ -1,124 +0,0 @@ -, clients: list} - */ - public static function getQuestionnaireResults( - PDO $pdo, - string $questionnaireID, - array $tokenRecord, - ?string $clientCode = null - ): array { - $qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id"); - $qn->execute([':id' => $questionnaireID]); - $questionnaire = $qn->fetch(PDO::FETCH_ASSOC); - - if (!$questionnaire) { - return ['questionnaire' => null, 'questions' => [], 'clients' => []]; - } - - $qStmt = $pdo->prepare(" - SELECT questionID, defaultText, type, orderIndex, isRequired - FROM question WHERE questionnaireID = :id ORDER BY orderIndex - "); - $qStmt->execute([':id' => $questionnaireID]); - $questions = $qStmt->fetchAll(PDO::FETCH_ASSOC); - $questionIDs = array_column($questions, 'questionID'); - - foreach ($questions as &$q) { - $q['isRequired'] = (int) $q['isRequired']; - $q['orderIndex'] = (int) $q['orderIndex']; - - $ao = $pdo->prepare(" - SELECT answerOptionID, defaultText, points, orderIndex - FROM answer_option WHERE questionID = :qid ORDER BY orderIndex - "); - $ao->execute([':qid' => $q['questionID']]); - $q['answerOptions'] = $ao->fetchAll(PDO::FETCH_ASSOC); - - foreach ($q['answerOptions'] as &$opt) { - $opt['points'] = (int) $opt['points']; - $opt['orderIndex'] = (int) $opt['orderIndex']; - } - unset($opt); - } - unset($q); - - [$rbacClause, $rbacParams] = rbac_client_filter($tokenRecord, 'cl'); - - $sql = " - SELECT cl.clientCode, cl.coachID, - co.username AS coachUsername, - sv.username AS supervisorUsername, - cq.status, cq.startedAt, cq.completedAt, cq.sumPoints, cq.assignedByCoach - FROM client cl - INNER JOIN completed_questionnaire cq - ON cq.clientCode = cl.clientCode AND cq.questionnaireID = :qnid - LEFT JOIN coach co ON co.coachID = cl.coachID - LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID - WHERE $rbacClause - "; - $params = array_merge([':qnid' => $questionnaireID], $rbacParams); - - if ($clientCode) { - $sql .= " AND cl.clientCode = :cc"; - $params[':cc'] = $clientCode; - } - $sql .= " ORDER BY cl.clientCode"; - - $cStmt = $pdo->prepare($sql); - $cStmt->execute($params); - $clients = $cStmt->fetchAll(PDO::FETCH_ASSOC); - - if (!empty($questionIDs) && !empty($clients)) { - $qPlaceholders = implode(',', array_fill(0, count($questionIDs), '?')); - $answerStmt = $pdo->prepare(" - SELECT clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt - FROM client_answer - WHERE clientCode = ? AND questionID IN ($qPlaceholders) - "); - - foreach ($clients as &$c) { - self::castClientInts($c); - - $answerStmt->execute(array_merge([$c['clientCode']], $questionIDs)); - $answers = $answerStmt->fetchAll(PDO::FETCH_ASSOC); - $answerMap = []; - foreach ($answers as $a) { - $answerMap[$a['questionID']] = [ - 'answerOptionID' => $a['answerOptionID'], - 'freeTextValue' => $a['freeTextValue'], - 'numericValue' => $a['numericValue'] !== null ? (float) $a['numericValue'] : null, - 'answeredAt' => $a['answeredAt'] !== null ? (int) $a['answeredAt'] : null, - ]; - } - $c['answers'] = $answerMap; - } - unset($c); - } else { - foreach ($clients as &$c) { - self::castClientInts($c); - $c['answers'] = (object) []; - } - unset($c); - } - - return [ - 'questionnaire' => $questionnaire, - 'questions' => $questions, - 'clients' => $clients, - ]; - } - - private static function castClientInts(array &$c): void - { - $c['sumPoints'] = $c['sumPoints'] !== null ? (int) $c['sumPoints'] : null; - $c['startedAt'] = $c['startedAt'] !== null ? (int) $c['startedAt'] : null; - $c['completedAt'] = $c['completedAt'] !== null ? (int) $c['completedAt'] : null; - } -} diff --git a/lib/repositories/SessionRepo.php b/lib/repositories/SessionRepo.php deleted file mode 100644 index 8971a37..0000000 --- a/lib/repositories/SessionRepo.php +++ /dev/null @@ -1,61 +0,0 @@ -prepare(" - INSERT INTO session (token, userID, role, entityID, createdAt, expiresAt, temp) - VALUES (:t, :uid, :role, :eid, :ca, :ea, :tmp) - ")->execute([ - ':t' => $token, - ':uid' => $userID, - ':role' => $role, - ':eid' => $entityID, - ':ca' => $now, - ':ea' => $now + $ttl, - ':tmp' => (int) $temp, - ]); - } - - /** - * @return array|null Null when token is missing or expired. - */ - public static function findByToken(PDO $pdo, string $token): ?array - { - $stmt = $pdo->prepare( - "SELECT * FROM session WHERE token = :t AND expiresAt >= :now" - ); - $stmt->execute([':t' => $token, ':now' => time()]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - return $row ?: null; - } - - public static function revoke(PDO $pdo, string $token): void - { - $pdo->prepare("DELETE FROM session WHERE token = :t") - ->execute([':t' => $token]); - } - - /** - * Remove all expired sessions. - * @return int Number of rows deleted. - */ - public static function cleanup(PDO $pdo): int - { - $stmt = $pdo->prepare("DELETE FROM session WHERE expiresAt < :now"); - $stmt->execute([':now' => time()]); - - return $stmt->rowCount(); - } -} diff --git a/lib/repositories/TranslationRepo.php b/lib/repositories/TranslationRepo.php deleted file mode 100644 index b3281c3..0000000 --- a/lib/repositories/TranslationRepo.php +++ /dev/null @@ -1,103 +0,0 @@ - ['table' => 'question_translation', 'pk' => 'questionID'], - 'answer_option' => ['table' => 'answer_option_translation', 'pk' => 'answerOptionID'], - 'string' => ['table' => 'string_translation', 'pk' => 'stringKey'], - ]; - - /** - * @return list - */ - public static function listLanguages(PDO $pdo): array - { - return $pdo->query( - "SELECT languageCode, name FROM language ORDER BY languageCode" - )->fetchAll(PDO::FETCH_ASSOC); - } - - public static function upsertLanguage(PDO $pdo, string $code, string $name): void - { - $pdo->prepare(" - INSERT INTO language (languageCode, name) VALUES (:lc, :n) - ON CONFLICT(languageCode) DO UPDATE SET name = excluded.name - ")->execute([':lc' => $code, ':n' => $name]); - } - - /** - * Delete a language and all its translations across all three tables. - */ - public static function deleteLanguage(PDO $pdo, string $code): void - { - $pdo->prepare("DELETE FROM language WHERE languageCode = :lc") - ->execute([':lc' => $code]); - $pdo->prepare("DELETE FROM question_translation WHERE languageCode = :lc") - ->execute([':lc' => $code]); - $pdo->prepare("DELETE FROM answer_option_translation WHERE languageCode = :lc") - ->execute([':lc' => $code]); - $pdo->prepare("DELETE FROM string_translation WHERE languageCode = :lc") - ->execute([':lc' => $code]); - } - - /** - * Get all translations for a given entity. - * - * @param string $type question|answer_option|string - * @return list - */ - public static function getForEntity(PDO $pdo, string $type, string $id): array - { - $meta = self::meta($type); - - $stmt = $pdo->prepare( - "SELECT languageCode, text FROM {$meta['table']} WHERE {$meta['pk']} = :id" - ); - $stmt->execute([':id' => $id]); - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * Insert or update a single translation. - * - * @param string $type question|answer_option|string - */ - public static function upsert(PDO $pdo, string $type, string $id, string $lang, string $text): void - { - $meta = self::meta($type); - - $pdo->prepare(" - INSERT INTO {$meta['table']} ({$meta['pk']}, languageCode, text) - VALUES (:id, :lang, :t) - ON CONFLICT({$meta['pk']}, languageCode) DO UPDATE SET text = excluded.text - ")->execute([':id' => $id, ':lang' => $lang, ':t' => $text]); - } - - /** - * Delete a single translation. - * - * @param string $type question|answer_option|string - */ - public static function delete(PDO $pdo, string $type, string $id, string $lang): void - { - $meta = self::meta($type); - - $pdo->prepare( - "DELETE FROM {$meta['table']} WHERE {$meta['pk']} = :id AND languageCode = :lang" - )->execute([':id' => $id, ':lang' => $lang]); - } - - /** - * @return array{table: string, pk: string} - */ - private static function meta(string $type): array - { - if (!isset(self::TABLE_MAP[$type])) { - throw new InvalidArgumentException("Unknown translation type: $type"); - } - - return self::TABLE_MAP[$type]; - } -} diff --git a/lib/repositories/UserRepo.php b/lib/repositories/UserRepo.php deleted file mode 100644 index 5daf572..0000000 --- a/lib/repositories/UserRepo.php +++ /dev/null @@ -1,211 +0,0 @@ -> - */ - public static function listAll(PDO $pdo): array - { - return $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); - } - - /** - * Coaches that belong to the given supervisor. - * @return list> - */ - public static function listBySupervisor(PDO $pdo, string $supervisorID): array - { - $svName = self::getSupervisorName($pdo, $supervisorID); - - $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' => $supervisorID, ':svname' => $svName]); - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * @return list - */ - public static function listSupervisors(PDO $pdo): array - { - return $pdo->query( - "SELECT supervisorID, username, location FROM supervisor ORDER BY username" - )->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * @return array|null - */ - public static function findByUsername(PDO $pdo, string $username): ?array - { - $stmt = $pdo->prepare("SELECT * FROM users WHERE username = :u"); - $stmt->execute([':u' => $username]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - return $row ?: null; - } - - /** - * @return array{role: string, entityID: string}|null - */ - public static function findById(PDO $pdo, string $userID): ?array - { - $stmt = $pdo->prepare("SELECT role, entityID FROM users WHERE userID = :uid"); - $stmt->execute([':uid' => $userID]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - return $row ?: null; - } - - public static function usernameExists(PDO $pdo, string $username): bool - { - $stmt = $pdo->prepare("SELECT 1 FROM users WHERE username = :u"); - $stmt->execute([':u' => $username]); - - return (bool) $stmt->fetch(); - } - - public static function supervisorExists(PDO $pdo, string $supervisorID): bool - { - $stmt = $pdo->prepare("SELECT 1 FROM supervisor WHERE supervisorID = :sid"); - $stmt->execute([':sid' => $supervisorID]); - - return (bool) $stmt->fetch(); - } - - public static function coachBelongsToSupervisor(PDO $pdo, string $coachEntityID, string $supervisorID): bool - { - $stmt = $pdo->prepare( - "SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :svid" - ); - $stmt->execute([':cid' => $coachEntityID, ':svid' => $supervisorID]); - - return (bool) $stmt->fetch(); - } - - /** - * Insert a new user inside a transaction: role-specific table first, then users. - * Caller is responsible for wrapping in qdb_open(true) / qdb_save(). - */ - public static function create( - PDO $pdo, - string $userID, - string $entityID, - string $username, - string $passwordHash, - string $role, - string $location, - string $supervisorID, - int $mustChangePassword - ): void { - $pdo->beginTransaction(); - try { - 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' => $passwordHash, - ':role' => $role, - ':eid' => $entityID, - ':mcp' => $mustChangePassword, - ':now' => time(), - ]); - - $pdo->commit(); - } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - throw $e; - } - } - - /** - * Delete user + role-specific row in a transaction. - */ - public static function delete(PDO $pdo, string $userID, string $role, string $entityID): void - { - $pdo->beginTransaction(); - try { - $pdo->prepare("DELETE FROM users WHERE userID = :uid") - ->execute([':uid' => $userID]); - - switch ($role) { - case 'admin': - $pdo->prepare("DELETE FROM admin WHERE adminID = :id") - ->execute([':id' => $entityID]); - break; - case 'supervisor': - $pdo->prepare("DELETE FROM supervisor WHERE supervisorID = :id") - ->execute([':id' => $entityID]); - break; - case 'coach': - $pdo->prepare("DELETE FROM coach WHERE coachID = :id") - ->execute([':id' => $entityID]); - break; - } - - $pdo->commit(); - } catch (Throwable $e) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - throw $e; - } - } - - public static function updatePassword(PDO $pdo, string $userID, string $newHash): void - { - $pdo->prepare( - "UPDATE users SET passwordHash = :h, mustChangePassword = 0 WHERE userID = :uid" - )->execute([':h' => $newHash, ':uid' => $userID]); - } - - /** - * @return string Username or empty string if not found. - */ - public static function getSupervisorName(PDO $pdo, string $supervisorID): string - { - $stmt = $pdo->prepare("SELECT username FROM supervisor WHERE supervisorID = :svid"); - $stmt->execute([':svid' => $supervisorID]); - - return $stmt->fetchColumn() ?: ''; - } -} diff --git a/lib/response.php b/lib/response.php index 35a2917..dcd1f68 100644 --- a/lib/response.php +++ b/lib/response.php @@ -1,5 +1,7 @@ getMessage()); + json_error('DECRYPT_FAILED', 'Could not decrypt request payload', 400); + } + $data = json_decode($plain, true); + if (!is_array($data)) { + json_error('INVALID_BODY', 'Decrypted body must be valid JSON object', 400); + } + return $data; +} + +function json_success_sensitive(mixed $data, string $tokenHex, int $status = 200): never { + $plain = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); + json_success(qdb_sensitive_envelope($plain, $tokenHex), $status); +} diff --git a/login.php b/login.php deleted file mode 100644 index 207dda6..0000000 --- a/login.php +++ /dev/null @@ -1,85 +0,0 @@ - false, "message" => "Username/password missing"]); - exit; -} - -try { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - - $stmt = $pdo->prepare( - "SELECT userID, passwordHash, role, entityID, mustChangePassword - FROM users WHERE username = :u" - ); - $stmt->execute([':u' => $username]); - $user = $stmt->fetch(PDO::FETCH_ASSOC); - $pdo = null; - qdb_discard($tmpDb, $lockFp); - - if (!$user) { - http_response_code(401); - echo json_encode(["success" => false, "message" => "Invalid credentials"]); - exit; - } - - if (!password_verify($password, $user['passwordHash'])) { - http_response_code(401); - echo json_encode(["success" => false, "message" => "Invalid credentials"]); - exit; - } - - if (($user['role'] ?? '') === 'coach') { - http_response_code(403); - echo json_encode([ - "success" => false, - "message" => "Coach accounts can only sign in through the mobile app.", - ]); - exit; - } - - if ((int)$user['mustChangePassword'] === 1) { - $tempToken = bin2hex(random_bytes(32)); - token_add($tempToken, 10 * 60, [ - 'role' => $user['role'], - 'entityID' => $user['entityID'], - 'userID' => $user['userID'], - 'temp' => true, - ]); - echo json_encode([ - "success" => true, - "user" => $username, - "role" => $user['role'], - "must_change_password" => true, - "temp_token" => $tempToken, - "message" => "Please change your password." - ]); - exit; - } - - $token = bin2hex(random_bytes(32)); - token_add($token, 30 * 24 * 60 * 60, [ - 'role' => $user['role'], - 'entityID' => $user['entityID'], - 'userID' => $user['userID'], - ]); - echo json_encode([ - "success" => true, - "token" => $token, - "user" => $username, - "role" => $user['role'], - ]); - -} catch (Throwable $e) { - http_response_code(500); - echo json_encode(["success" => false, "message" => "Server error"]); -} diff --git a/manage_users.php b/manage_users.php deleted file mode 100644 index c00b9c8..0000000 --- a/manage_users.php +++ /dev/null @@ -1,236 +0,0 @@ -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"]); diff --git a/questions_en.json b/questions_en.json deleted file mode 100644 index 09b9c75..0000000 --- a/questions_en.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "client_code": "Client code", - "questionnaire_1_demographic_information": "Questionnaire status", - "questionnaire_1_demographic_information-coach_code": "Coach Code", - "questionnaire_1_demographic_information-consent_instruction": "Obtain client consent (separate document)", - "questionnaire_1_demographic_information-no_consent_entered": "You have indicated that the client has not signed the consent form.", - "questionnaire_1_demographic_information-accommodation": "Accommodation", - "questionnaire_1_demographic_information-other_accommodation": "Other accommodation (name)", - "questionnaire_1_demographic_information-client_code_entry_question": "Please enter client code and coach code.", - "questionnaire_1_demographic_information-age": "Age: How old are you?", - "questionnaire_1_demographic_information-gender": "Gender", - "questionnaire_1_demographic_information-country_of_origin": "Country of origin: where are you from?", - "questionnaire_1_demographic_information-departure_country": "When did you leave your country of origin?", - "questionnaire_1_demographic_information-since_in_germany": "Since when have you been in Germany?", - "questionnaire_1_demographic_information-living_situation": "How are you living here?", - "questionnaire_1_demographic_information-number_family_members": "How many family members are you living with?", - "questionnaire_1_demographic_information-languages_spoken": "Which languages do you speak?", - "questionnaire_1_demographic_information-german_skills": "How would you rate your German language skills?", - "questionnaire_1_demographic_information-school_years_total": "How many years did you attend school?", - "questionnaire_1_demographic_information-school_years_origin": "How many years did you attend school? (in your home country)", - "questionnaire_1_demographic_information-school_years_transit": "How many years did you attend school? (while in transit)", - "questionnaire_1_demographic_information-school_years_germany": "How many years did you attend school? (in Germany)", - "questionnaire_1_demographic_information-vocational_training": "Have you completed vocational training?", - "questionnaire_1_demographic_information-provisional_accommodation_since": "Since when have you been in provisional accommodation?", - "questionnaire_2_rhs": "Questionnaire status", - "questionnaire_2_rhs-coach_code": "Coach Code", - "questionnaire_2_rhs-glass_explanation": "The response options are shown in words and as a picture of a glass container: if something does not apply at all, the glass is empty; if it applies somewhat or fairly, the glass fills up; if it applies extremely, the glass is full.", - "questionnaire_2_rhs-q1_symptom": "1. Muscle, bone, or joint pain", - "questionnaire_2_rhs-q2_symptom": "2. Feeling unhappy, sad, or depressed most of the time", - "questionnaire_2_rhs-q3_symptom": "3. Overthinking or worrying too much", - "questionnaire_2_rhs-q4_symptom": "4. Feeling helpless", - "questionnaire_2_rhs-q5_symptom": "5. Being easily startled for no reason", - "questionnaire_2_rhs-q6_symptom": "6. Fatigue, dizziness, or feeling weak", - "questionnaire_2_rhs-q7_symptom": "7. Feeling nervous or insecure", - "questionnaire_2_rhs-q8_symptom": "8. Feeling restless, unable to sit still", - "questionnaire_2_rhs-q9_symptom": "9. Feeling like crying easily", - "questionnaire_2_rhs-q10_reexperience_trauma": "10. ... re-experiencing the trauma; behaving or feeling as if it were happening again?", - "questionnaire_2_rhs-q11_physical_reaction": "11. ... experiencing physical reactions (e.g., sweating, rapid heartbeat) when reminded of the trauma?", - "questionnaire_2_rhs-q12_emotional_numbness": "12. ... feeling emotionally numb (e.g., feeling sad but unable to cry, unable to feel loving emotions)?", - "questionnaire_2_rhs-q13_easily_startled": "13. ... being more easily startled (e.g., when someone approaches from behind)?", - "questionnaire_2_rhs-q14_intro": "14. Thinking about your life in the past four weeks, to what extent do you feel that you:", - "questionnaire_2_rhs-pain_rating_instruction": "Choose the number (0–10) that best describes how much suffering the respondent experienced in the past week, including today.", - "questionnaire_2_rhs-violence_question_1": "Have you ever been injured by others so seriously that you needed medical attention? (doctor, hospital)", - "questionnaire_2_rhs-times_happend": "Has this ... happened?", - "questionnaire_2_rhs-age_at_incident": "At what age: at ... years?", - "questionnaire_2_rhs-conflict_since_arrival": "Since arriving in Germany, have you ever been involved in violent conflicts (physical fights, physical or psychological conflicts)?", - "questionnaire_2_rhs-times_happend2": "Has this ... happened?", - "questionnaire_2_rhs-age_at_incident2": "At what age: at ... years?", - "questionnaire_2_rhs-asylum_procedure_since": "Since when has your asylum procedure been ongoing?", - "questionnaire_3_integration_index": "Questionnaire status", - "questionnaire_3_integration_index-coach_code": "Coach Code", - "questionnaire_3_integration_index-feeling_connected_to_germany_question": "How connected do you feel to Germany (how well have you settled in Germany)?", - "questionnaire_3_integration_index-understanding_political_issues": "How well do you understand the most important political issues in Germany?", - "questionnaire_3_integration_index-unexpected_expense_question": "Imagine you had to make an unexpected but necessary payment. Which of the following amounts could you pay or afford (if you had free access to money)?", - "questionnaire_3_integration_index-dining_with_germans_question": "How often have you dined (e.g. dinner) with Germans in the past 12 months?", - "questionnaire_3_integration_index-reading_german_articles_question": "How well can you read simple newspaper articles in German about familiar topics (health, home, family, etc.) and understand the main points?", - "questionnaire_3_integration_index-visiting_doctor_question": "How easy or difficult would it be for you to see a doctor in this country? (making an appointment, etc. - not: 'you can't get an appointment', 'I would never go to a doctor here')", - "questionnaire_3_integration_index-feeling_as_outsider_question": "How often do you feel like an outsider in Germany?", - "questionnaire_3_integration_index-recent_occupation_question": "How have you been occupied in the last four weeks? (Please select only one answer)", - "questionnaire_3_integration_index-discussing_politics_question": "Since arriving in Germany, how often have you discussed important political topics in Germany with other people?", - "questionnaire_3_integration_index-contact_with_germans_question": "With how many Germans have you exchanged messages (phone, messenger chat (WhatsApp, Facebook...), or SMS) in the last 4 weeks?", - "questionnaire_3_integration_index-speaking_german_opinion_question": "How well can you participate in a conversation in German on familiar topics (children, family, school, home, etc.) and express your opinion?", - "questionnaire_3_integration_index-job_search_question": "How easy or difficult would it be for you to search for and find a job in this country (even if you may not be allowed to work: what would be your assumption if there were no restrictions)?", - "questionnaire_4_consultation_results": "Questionnaire status", - "questionnaire_4_consultation_results-coach_code": "Coach Code", - "questionnaire_4_consultation_results-date_consultation_health_interview_result": "Date of counseling interview (health interview result green/yellow/red)", - "questionnaire_4_consultation_results-consultation_decision": "Counseling decision (0)", - "questionnaire_4_consultation_results-consent_conversation_in_6_months": "Consent for conversation in 6 months:", - "questionnaire_4_consultation_results-participation_in_coaching": "Participation In Coaching", - "questionnaire_4_consultation_results-consent_coaching_given": "Consent for coaching is given", - "questionnaire_4_consultation_results-decision_after_reflection_period": "Decision on .............. (date) after reflection period", - "questionnaire_4_consultation_results-professional_referral": "Professional Referral", - "questionnaire_4_consultation_results-confidentiality_agreement": "Confidentiality Agreement", - "questionnaire_4_consultation_results-health_insurance_card": "Health Insurance Card", - "questionnaire_5_final_interview": "Final Interview", - "questionnaire_5_final_interview-coach_code": "Coach Code", - "questionnaire_5_final_interview-consent_followup_6_months": "Consent for conversation in 6 months", - "questionnaire_5_final_interview-date_final_interview": "Date of final interview", - "questionnaire_5_final_interview-amount_nat_appointments": "Number of NAT appointments (after the counseling interview)", - "questionnaire_5_final_interview-amount_session_flowers": "Number of storytelling sessions with flowers", - "questionnaire_5_final_interview-amount_session_stones": "Number of storytelling sessions with stones", - "questionnaire_5_final_interview-termination_nat_coaching": "Termination of NAT coaching", - "questionnaire_5_final_interview-client_canceled_NAT": "Client discontinued NAT", - "questionnaire_6_follow_up_survey": "Questionnaire status", - "questionnaire_6_follow_up_survey-coach_code": "Coach Code", - "questionnaire_6_follow_up_survey-follow_up": "Follow-up survey", - "questionnaire_6_follow_up_survey-special_burden_question": "Was there any special burden?", - "questionnaire_6_follow_up_survey-glass_explanation": "The response options are shown in words and as a picture of a glass container: if something does not apply at all, the glass is empty; if it applies somewhat or fairly, the glass fills up; if it applies extremely, the glass is full.", - "questionnaire_6_follow_up_survey-how_strong_past_month": "How strongly did you experience the following in the past month...", - "questionnaire_6_follow_up_survey-q14_intro": "14. Thinking about your life in the past four weeks, to what extent do you feel that you:", - "questionnaire_6_follow_up_survey-pain_rating_instruction": "Choose the number (0–10) that best describes how much suffering the respondent experienced in the past week, including today.", - "questionnaire_6_follow_up_survey-feeling_connected_to_germany_question": "How connected do you feel to Germany (how well have you settled in Germany)?", - "questionnaire_6_follow_up_survey-understanding_political_issues": "How well do you understand the most important political issues in Germany?", - "questionnaire_6_follow_up_survey-unexpected_expense_question": "Imagine you had to make an unexpected but necessary payment. Which of the following amounts could you pay or afford (if you had free access to money)?", - "questionnaire_6_follow_up_survey-dining_with_germans_question": "How often have you dined (e.g. dinner) with Germans in the past 12 months?", - "questionnaire_6_follow_up_survey-reading_german_articles_question": "How well can you read simple newspaper articles in German about familiar topics (health, home, family, etc.) and understand the main points?", - "questionnaire_6_follow_up_survey-visiting_doctor_question": "How easy or difficult would it be for you to see a doctor in this country? (making an appointment, etc. - not: 'you can't get an appointment', 'I would never go to a doctor here')", - "questionnaire_6_follow_up_survey-feeling_as_outsider_question": "How often do you feel like an outsider in Germany?", - "questionnaire_6_follow_up_survey-recent_occupation_question": "How have you been occupied in the last four weeks? (Please select only one answer)", - "questionnaire_6_follow_up_survey-discussing_politics_question": "Since arriving in Germany, how often have you discussed important political topics in Germany with other people?", - "questionnaire_6_follow_up_survey-contact_with_germans_question": "With how many Germans have you exchanged messages (phone, messenger chat (WhatsApp, Facebook...), or SMS) in the last 4 weeks?", - "questionnaire_6_follow_up_survey-speaking_german_opinion_question": "How well can you participate in a conversation in German on familiar topics (children, family, school, home, etc.) and express your opinion?", - "questionnaire_6_follow_up_survey-job_search_question": "How easy or difficult would it be for you to search for and find a job in this country (even if you may not be allowed to work: what would be your assumption if there were no restrictions)?" -} diff --git a/seed_user.php b/seed_user.php deleted file mode 100644 index 5a3f2d4..0000000 --- a/seed_user.php +++ /dev/null @@ -1,176 +0,0 @@ - [location] -// php seed_user.php supervisor [location] -// php seed_user.php coach -// -// Flags: -// --force-change Sets mustChangePassword = 1 (default ON, use --no-force-change to skip) -// --no-force-change Sets mustChangePassword = 0 - -if (php_sapi_name() !== 'cli') { - http_response_code(403); - echo "CLI only\n"; - exit(1); -} - -require_once __DIR__ . '/db_init.php'; - -$usage = << [location] - php seed_user.php supervisor [location] - php seed_user.php coach - -Options: - --no-force-change Skip requiring password change on first login (default: require change) - --list List all existing users instead of creating one - -USAGE; - -// Strip flag args -$args = []; -$mustChangePassword = 1; -$listMode = false; -foreach (array_slice($argv, 1) as $arg) { - if ($arg === '--no-force-change') { $mustChangePassword = 0; continue; } - if ($arg === '--force-change') { $mustChangePassword = 1; continue; } - if ($arg === '--list') { $listMode = true; continue; } - $args[] = $arg; -} - -// List mode -if ($listMode) { - try { - [$pdo, $tmpDb, $lockFp] = qdb_open(false); - $rows = $pdo->query( - "SELECT u.username, u.role, u.entityID, u.mustChangePassword, - COALESCE(a.location, s.location, c.supervisorID) AS detail - 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' - ORDER BY u.role, u.username" - )->fetchAll(PDO::FETCH_ASSOC); - qdb_discard($tmpDb, $lockFp); - - if (empty($rows)) { - echo "No users found.\n"; - exit(0); - } - printf("%-20s %-12s %-40s %s\n", 'Username', 'Role', 'EntityID', 'Detail / SupervisorID'); - echo str_repeat('-', 90) . "\n"; - foreach ($rows as $r) { - $mustChange = (int)$r['mustChangePassword'] ? ' [must change pw]' : ''; - printf("%-20s %-12s %-40s %s%s\n", - $r['username'], $r['role'], $r['entityID'], - $r['detail'] ?? '', $mustChange - ); - } - exit(0); - } catch (Throwable $e) { - fwrite(STDERR, "Error: " . $e->getMessage() . "\n"); - exit(1); - } -} - -if (count($args) < 3) { - fwrite(STDERR, $usage); - exit(1); -} - -$role = strtolower($args[0]); -$username = $args[1]; -$password = $args[2]; -$extra = $args[3] ?? ''; // location (admin/supervisor) or supervisorID (coach) - -if (!in_array($role, ['admin', 'supervisor', 'coach'], true)) { - fwrite(STDERR, "Error: role must be admin, supervisor, or coach.\n"); - exit(1); -} -if ($username === '' || $password === '') { - fwrite(STDERR, $usage); - exit(1); -} -if (strlen($password) < 6) { - fwrite(STDERR, "Error: Password must be at least 6 characters.\n"); - exit(1); -} -if ($role === 'coach' && $extra === '') { - fwrite(STDERR, "Error: coach requires a supervisorID as the 4th argument.\n"); - exit(1); -} - -try { - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - - // Duplicate username check - $chk = $pdo->prepare("SELECT userID FROM users WHERE username = :u"); - $chk->execute([':u' => $username]); - if ($chk->fetch()) { - qdb_discard($tmpDb, $lockFp); - fwrite(STDERR, "Error: User '$username' already exists.\n"); - exit(1); - } - - // For coach, verify supervisorID exists - if ($role === 'coach') { - $sv = $pdo->prepare("SELECT supervisorID FROM supervisor WHERE supervisorID = :sid"); - $sv->execute([':sid' => $extra]); - if (!$sv->fetch()) { - qdb_discard($tmpDb, $lockFp); - fwrite(STDERR, "Error: supervisorID '$extra' not found. Create the supervisor first.\n"); - exit(1); - } - } - - $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' => $extra]); - break; - case 'supervisor': - $pdo->prepare("INSERT INTO supervisor (supervisorID, username, location) VALUES (:id, :u, :loc)") - ->execute([':id' => $entityID, ':u' => $username, ':loc' => $extra]); - break; - case 'coach': - $pdo->prepare("INSERT INTO coach (coachID, supervisorID, username) VALUES (:id, :sid, :u)") - ->execute([':id' => $entityID, ':sid' => $extra, ':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' => $mustChangePassword, - ':now' => $now, - ]); - - $pdo->commit(); - $pdo = null; - qdb_save($tmpDb, $lockFp); - - $changeNote = $mustChangePassword ? ' (must change password on first login)' : ''; - echo ucfirst($role) . " '$username' created successfully$changeNote.\n"; - echo "EntityID: $entityID\n"; - -} catch (Throwable $e) { - if (isset($tmpDb, $lockFp)) qdb_discard($tmpDb, $lockFp); - fwrite(STDERR, "Error: " . $e->getMessage() . "\n"); - exit(1); -} diff --git a/tokens.jsonl b/tokens.jsonl deleted file mode 100644 index 86d235f..0000000 --- a/tokens.jsonl +++ /dev/null @@ -1,24 +0,0 @@ -{"token":"e27b08a83d8d034746ad3f860758aa20e3c302deef5da0df29a6ab93f71dda90","created":1774517813,"exp":1777109813} -{"token":"4215cb8aa644120fce31c8bd9ff3cb3e100b6ae2f19c83bba9e1c6a87e7f4e99","created":1775642692,"exp":1778234692,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"0441863e59bbc0bef45c1377ae31a7155c249cf298981dad3aaa7d9389635d95","created":1775643020,"exp":1778235020,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"d9813ed055fd974e92c92ffd2b9c64b86a572f4dfe0a075f6e68e4d057ae9629","created":1775643419,"exp":1778235419,"role":"coach","entityID":"f956a1c2239849b8924e9cf98da9c3da","userID":"3abc03d2bacc8e48fe62b6afc382d3a3"} -{"token":"b2277c780fb2c8ed218173c76bd7f36dce48bfb284862fdd04732fe84a9d3437","created":1775643440,"exp":1778235440,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"ac2d9596f485f6f15051f267299c24aac60bd743f020c60f250d5086b1ae6dba","created":1775643458,"exp":1778235458,"role":"supervisor","entityID":"d4538fc39664f037eb8b0d4ceb8ec07e","userID":"9e23fa6f1ed02bef3a7405d15e866bb9"} -{"token":"3ae1ea99a01abedd3e2dfe6e2a4e8095a127353bf738ffb7baa0ca14b49b3ee6","created":1775659764,"exp":1778251764,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"324478b89a99a8b6987131b26d729364ef7fa7cadb51634fae160b12b052bd49","created":1775661148,"exp":1778253148,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"40032db80223b526499a7553ac4b58ecb0276b4aac8023444de1c674c07cc394","created":1775740664,"exp":1778332664,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"70417759cda569e3c11b6a443431e536d7eda73268e338ab40f7500d056083bb","created":1775741768,"exp":1778333768,"role":"supervisor","entityID":"d4538fc39664f037eb8b0d4ceb8ec07e","userID":"9e23fa6f1ed02bef3a7405d15e866bb9"} -{"token":"f067dd4ca164b0fa66a87b299308390da0913da1b16cddad73c1b322272379b6","created":1775741791,"exp":1778333791,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"3b381d3013a68896662cad27196e77e4dd3dd0117a9ddf08922dbdff807d4fb8","created":1775741807,"exp":1778333807,"role":"supervisor","entityID":"d4538fc39664f037eb8b0d4ceb8ec07e","userID":"9e23fa6f1ed02bef3a7405d15e866bb9"} -{"token":"33cf888d8a443dd3055650a8962293e7251620d9a1a8a64a6a224611bc41d3b8","created":1775744411,"exp":1778336411,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"22f588244d7635bc6faf329a6291e91b3dd1ca301e2f2ec084a921e18a757ca4","created":1775744481,"exp":1778336481,"role":"admin","entityID":"3c7c7a1f91980d255c0adcbd1efd4dd7","userID":"4c8b919c5f102c68b2a255b70d541c74"} -{"token":"26c6ef3ebfbfd8f267f96a60bc4913504ade4b3edd0404668a2cf7af0dcb99e9","created":1775745540,"exp":1775746140,"role":"admin","entityID":"f7f8ab5e6f32eb469e5219c5561a1d71","userID":"d64e5796774a414c8159dcb786cd5847","temp":true} -{"token":"fadf7a06ead066c607d5714546b6f4827bd21b3a4542e68386fb34ace4bcaa53","created":1775745548,"exp":1778337548,"role":"admin","entityID":"f7f8ab5e6f32eb469e5219c5561a1d71","userID":"d64e5796774a414c8159dcb786cd5847"} -{"token":"9fcea9c8d98fa09274dbbf6741bea50ab174a01b9b5d26138f933adf1f29957c","created":1775745732,"exp":1775746332,"role":"admin","entityID":"2069fffc7091928c70826b6498eccd7c","userID":"5fab912e0108e6b872b02c5d755cc893","temp":true} -{"token":"20fbfb3a6f248a24ab33769a5e5de06732f402e0c9b9f709d17cc3b442c3b1ba","created":1775745738,"exp":1778337738,"role":"admin","entityID":"2069fffc7091928c70826b6498eccd7c","userID":"5fab912e0108e6b872b02c5d755cc893"} -{"token":"e88d0746883123156433e55a5e07044e253f3429472df78e5d61f43c2c40e760","created":1775745809,"exp":1775746409,"role":"supervisor","entityID":"0032669cbb342c7c9937e48848e916b2","userID":"766621a2fce45e977f999926d9196e3d","temp":true} -{"token":"4cc8d74f0fe3d655e16c337612d02a68208620169c6d7ca587a25fddce61c004","created":1775745814,"exp":1778337814,"role":"supervisor","entityID":"0032669cbb342c7c9937e48848e916b2","userID":"766621a2fce45e977f999926d9196e3d"} -{"token":"2f5d7b59ccb55b21d4a5b7d7e03c9966df5c1377f447098ab398e9b789a6a188","created":1775746162,"exp":1778338162,"role":"admin","entityID":"f7f8ab5e6f32eb469e5219c5561a1d71","userID":"d64e5796774a414c8159dcb786cd5847"} -{"token":"f1f2ba5d1a6e898688658006c8c42c0f6cb8a6e214f08d016cfc1a2a72af1e8e","created":1775747842,"exp":1778339842,"role":"admin","entityID":"f7f8ab5e6f32eb469e5219c5561a1d71","userID":"d64e5796774a414c8159dcb786cd5847"} -{"token":"02ded9879607050fc2531d8fb9265faadcc4c0a6ffced7a82eb919a0354f9c9c","created":1775804416,"exp":1778396416,"role":"admin","entityID":"f7f8ab5e6f32eb469e5219c5561a1d71","userID":"d64e5796774a414c8159dcb786cd5847"} -{"token":"4923c95ed744e8eee5d4f215b16c2a1141ec49bca7f616651f124865e06e4e1b","created":1775835537,"exp":1778427537,"role":"admin","entityID":"f7f8ab5e6f32eb469e5219c5561a1d71","userID":"d64e5796774a414c8159dcb786cd5847"} diff --git a/translations_en.json b/translations_en.json deleted file mode 100644 index 4b11f0d..0000000 --- a/translations_en.json +++ /dev/null @@ -1,349 +0,0 @@ -{ - "client_code_entry_question": "Please enter client code and coach code.", - "all_fields_between_0_and_15": "Please enter a number between 0 and 15 in all fields!", - "fill_both_fields": "Please fill in both fields!", - "please_answer_all": "Please answer all questions!", - "select_accommodation": "Please select an accommodation!", - "enter_coach_code": "Please enter your coach code to continue!", - "enter_country": "Please enter a country to continue!", - "only_letters_allowed": "Please enter letters only!", - "enter_valid_number": "Please enter a valid number!", - "enter_valid_year": "Please enter a valid year!", - "enter_text_to_continue": "Please enter text to continue!", - "select_at_least_one_language": "Please select at least one language!", - "select_country": "Please select a country", - "select_gender": "Please select a gender!", - "select_one_answer": "Please select one answer!", - "select_one_option": "Please select one option!", - "coach_code": "Coach Code", - "client_code": "Client Code", - "select_month": "Select month!", - "select_year": "Select year!", - "choose_answer": "Choose answer", - "choose_option": "Choose option!", - "consent_not_signed": "Consent not signed", - "consent_signed": "Consent signed", - "error_file": "Error processing file!", - "no_school_attended": "I did not attend school!", - "once": "once", - "year_after_2000": "The year must be after 2000!", - "year_after_departure": "The year must be after leaving the country of origin!", - "year_max": "The year must be less than or equal to $MAX_VALUE_YEAR!", - "data_final_warning": "Important: The data cannot be changed or edited after completion!", - "multiple_times": "multiple times", - "more_than_15_years": "more than 15 years", - "no": "No", - "no_answer": "No answer", - "other_country": "Other country", - "value_must_be_less_equal_max": "The value must be less than or equal to $MAX_VALUE_AGE!", - "value_between_1_and_15": "The value must be between 1 and 15!", - "invalid_month": "Invalid month!", - "invalid_year": "Invalid year!", - "yes": "Yes", - "next": "Next", - "previous": "Back", - "finish": "Complete data entry", - "thank_you_participation": "Thank you for participating in this survey.", - "response_recorded": "Your response has been recorded.", - "january": "January", - "february": "February", - "march": "March", - "april": "April", - "may": "May", - "june": "June", - "july": "July", - "august": "August", - "september": "September", - "october": "October", - "november": "November", - "december": "December", - "consent_instruction": "Obtain client consent (separate document)", - "no_consent_entered": "You have indicated that the client has not signed the consent form.", - "no_consent_note": "This is important information for the evaluation of 'BW schützt!'.", - "coach_code_request": "Please enter your coach code and the name of the accommodation below. Then upload the data as usual.", - "other_accommodation": "Other accommodation (name)", - "age": "Age: How old are you?", - "gender": "Gender", - "gender_male": "Male", - "gender_female": "Female", - "gender_diverse": "Diverse", - "country_of_origin": "Country of origin: where are you from?", - "country_text_entry": "Country of origin (text entry)", - "departure_country": "When did you leave your country of origin?", - "year": "Year", - "since_in_germany": "Since when have you been in Germany?", - "living_situation": "How are you living here?", - "alone": "Alone", - "with_family": "With family members", - "number_family_members": "How many family members are you living with?", - "number_label": "Number of family members", - "languages_spoken": "Which languages do you speak?", - "language_albanian": "Albanian", - "language_pashto": "Pashto", - "language_russian": "Russian", - "language_serbo": "Serbo-Croatian", - "language_somali": "Somali", - "language_tamil": "Tamil", - "language_tigrinya": "Tigrinya", - "language_turkish": "Turkish", - "language_ukrainian": "Ukrainian", - "language_urdu": "Urdu", - "language_other": "Other", - "language_arabic": "Arabic", - "language_dari_farsi": "Dari/Farsi", - "language_chinese": "Chinese", - "language_english": "English", - "language_macedonian": "Macedonian", - "language_kurmanji": "Kurdish-Kurmanji", - "language_hindi": "Hindi", - "language_french": "French", - "german_skills": "How would you rate your German language skills?", - "skill_none": "None", - "skill_basic": "Basic knowledge", - "skill_a1": "A1", - "skill_a2": "A2", - "skill_b1": "B1", - "skill_b2": "B2", - "skill_c1": "C1", - "skill_c2": "C2", - "school_years_origin": "How many years did you attend school? (in your home country)", - "school_years_transit": "How many years did you attend school? (while in transit)", - "school_years_germany": "How many years did you attend school? (in Germany)", - "label_years": "Years", - "vocational_training": "Have you completed vocational training?", - "answer_no": "No", - "answer_started": "Started", - "answer_completed": "Completed", - "provisional_accommodation_since": "Since when have you been in provisional accommodation?", - "asylum_procedure_since": "Since when has your asylum procedure been ongoing?", - "accommodation": "Accommodation", - "steinstrasse": "Steinstraße", - "doerfle": "Dörfle", - "zoll_emmishofer": "Zoll (Emmishofer Straße)", - "other": "Other", - "school_years_total": "How many years did you attend school?", - "glass_explanation": "The response options are shown in words and as a picture of a glass container: if something does not apply at all, the glass is empty; if it applies somewhat or fairly, the glass fills up; if it applies extremely, the glass is full.", - "symptoms": "Symptoms", - "never_glass": "Never", - "little_glass": "A little", - "moderate_glass": "Moderately", - "much_glass": "Much", - "extreme_glass": "Extremely", - "q1_symptom": "1. Muscle, bone, or joint pain", - "q2_symptom": "2. Feeling unhappy, sad, or depressed most of the time", - "q3_symptom": "3. Overthinking or worrying too much", - "q4_symptom": "4. Feeling helpless", - "q5_symptom": "5. Being easily startled for no reason", - "q6_symptom": "6. Fatigue, dizziness, or feeling weak", - "q7_symptom": "7. Feeling nervous or insecure", - "q8_symptom": "8. Feeling restless, unable to sit still", - "q9_symptom": "9. Feeling like crying easily", - "how_strong_past_month": "How strongly did you experience the following in the past month...", - "q10_reexperience_trauma": "10. ... re-experiencing the trauma; behaving or feeling as if it were happening again?", - "q11_physical_reaction": "11. ... experiencing physical reactions (e.g., sweating, rapid heartbeat) when reminded of the trauma?", - "q12_emotional_numbness": "12. ... feeling emotionally numb (e.g., feeling sad but unable to cry, unable to feel loving emotions)?", - "q13_easily_startled": "13. ... being more easily startled (e.g., when someone approaches from behind)?", - "instruction_listen_statements": "I will now read 5 statements to you. Please listen carefully and then tell me which one best applies to you:", - "q14_intro": "14. Thinking about your life in the past four weeks, to what extent do you feel that you:", - "q14_option_1": "Are able to handle everything that comes your way", - "q14_option_2": "Are able to handle most things that come your way", - "q14_option_3": "Are able to handle some things but not others", - "q14_option_4": "Are unable to handle most things", - "q14_option_5": "Are unable to handle anything", - "pain_rating_instruction": "Choose the number (0–10) that best describes how much suffering the respondent experienced in the past week, including today.", - "violence_intro": "Finally, I have 2 questions about experiences of violence:", - "violence_question_1": "Have you ever been injured by others so seriously that you needed medical attention? (doctor, hospital)", - "times_happend": "Has this ... happened?", - "age_at_incident": "At what age: at ... years?", - "times_happend2": "Has this ... happened?", - "age_at_incident2": "At what age: at ... years?", - "conflict_since_arrival": "Since arriving in Germany, have you ever been involved in violent conflicts (physical fights, physical or psychological conflicts)?", - "finish_data_entry": "When you have finished entering the data, click \"Save\".", - "feeling_connected_to_germany_question": "How connected do you feel to Germany (how well have you settled in Germany)?", - "intro_life_in_germany": "Now we would like to ask you some questions about your life in Germany:", - "not_connected_at_all": "not connected at all", - "somewhat_loose": "somewhat loose", - "moderately_connected": "moderately connected", - "quite_connected": "quite connected", - "very_connected": "very connected", - "understanding_political_issues": "How well do you understand the most important political issues in Germany?", - "very_good": "very good", - "good": "good", - "fairly_good": "fairly good", - "somewhat": "somewhat", - "not_at_all": "not at all", - "unexpected_expense_question": "Imagine you had to make an unexpected but necessary payment. Which of the following amounts could you pay or afford (if you had free access to money)?", - "expense_50": "up to €50", - "expense_100": "€100", - "expense_300": "€300", - "expense_500": "€500", - "expense_800": "€800", - "dining_with_germans_question": "How often have you dined (e.g. dinner) with Germans in the past 12 months?", - "never": "never", - "once_a_year": "once a year", - "once_a_month": "once a month", - "once_a_week": "once a week", - "almost_every_day": "almost every day", - "reading_german_articles_question": "How well can you read simple newspaper articles in German about familiar topics (health, home, family, etc.) and understand the main points?", - "not_good": "not well", - "visiting_doctor_question": "How easy or difficult would it be for you to see a doctor in this country? (making an appointment, etc. - not: 'you can't get an appointment', 'I would never go to a doctor here')", - "very_difficult": "very difficult", - "rather_difficult": "rather difficult", - "neither_nor": "neither difficult nor easy", - "rather_easy": "rather easy", - "very_easy": "very easy", - "feeling_as_outsider_question": "How often do you feel like an outsider in Germany?", - "rarely": "rarely", - "sometimes": "sometimes", - "often": "often", - "always": "always", - "recent_occupation_question": "How have you been occupied in the last four weeks? (Please select only one answer)", - "employed": "in paid employment, even if only temporarily (employed, self-employed, minor jobs)", - "education": "in education (school, vocational training, university)", - "internship": "internship", - "unemployed_searching": "unemployed and actively looking for work", - "unemployed_not_searching": "unemployed and not actively looking for work", - "sick_or_disabled": "ill or unable to work", - "unpaid_housework": "in unpaid housework, childcare / caring for others", - "other_activity": "Other (e.g., language course)", - "discussing_politics_question": "Since arriving in Germany, how often have you discussed important political topics in Germany with other people?", - "contact_with_germans_question": "With how many Germans have you exchanged messages (phone, messenger chat (WhatsApp, Facebook...), or SMS) in the last 4 weeks?", - "zero": "0", - "one_to_three": "1 to 3", - "three_to_six": "3 to 6", - "seven_to_fourteen": "7 to 14", - "fifteen_or_more": "15 or more", - "speaking_german_opinion_question": "How well can you participate in a conversation in German on familiar topics (children, family, school, home, etc.) and express your opinion?", - "moderately_good": "moderately good", - "job_search_question": "How easy or difficult would it be for you to search for and find a job in this country (even if you may not be allowed to work: what would be your assumption if there were no restrictions)?", - "integration_index": "Integration Index: %d", - "consent_followup_6_months": "Consent for conversation in 6 months", - "day": "Day", - "month": "Month", - "select_day": "Please select a day.", - "select_month": "Please select a month.", - "select_year": "Please select a year.", - "date_final_interview": "Date of final interview", - "amount_nat_appointments": "Number of NAT appointments (after the counseling interview)", - "amount_session_flowers": "Number of storytelling sessions with flowers", - "amount_session_stones": "Number of storytelling sessions with stones", - "termination_nat_coaching": "Termination of NAT coaching", - "termination_nat_regular": "Regular (mutual decision to terminate after discussing all important events)", - "termination_nat_referral": "Referral to a specialized professional (doctor, psychologist, ...)", - "termination_nat_dropout": "Client discontinued NAT", - "follow_up": "Follow-up survey", - "follow_up_completed": "was successfully conducted", - "follow_up_failed_contact": "could not be conducted: repeated failed contact attempts", - "follow_up_consent_withdrawn": "could not be conducted: consent withdrawn", - "special_burden_question": "Was there any special burden?", - "resilience_fully_capable": "Able to handle everything that comes your way", - "resilience_mostly_capable": "Able to handle most things that come your way", - "resilience_partially_capable": "Able to handle some things but not others", - "resilience_mostly_not_capable": "Unable to handle most things", - "resilience_not_capable": "Unable to handle anything", - "intro_resilience_questions": "I will now read 5 statements to you. Please listen carefully and then tell me which statement best applies to you:", - "resilience_reflection_prompt": "Thinking about your life in the past four weeks, to what extent do you feel that you:", - "resilience_some_capable_some_not": "Able to handle most things but unable to handle some others", - "follow_up_survey_score": "The RHS total score is: %d", - "select_one_answer_per_row": "Please select one answer per row!", - "no_next_question_defined": "No forwarding page defined", - "date_consultation_health_interview_result": "Date of counseling interview (health interview result green/yellow/red)", - "consultation_decision": "Counseling decision (RHS_POINTS)", - "consent_conversation_in_6_months": "Consent for conversation in 6 months:", - "participation_in_coaching": "Participation in coaching", - "decision_after_reflection_period": "Decision on .............. (date) after reflection period", - "participation_coaching": "Coaching participation", - "consent_coaching_given": "Consent for coaching is given", - "consent_form_must_be_available": "Consent form must be available!", - "professional_referral": "Professional referral", - "confidentiality_agreement": "Confidentiality agreement", - "health_insurance_card": "Health insurance card", - "green": "green", - "yellow": "yellow", - "red": "red", - "time_to_think_about_it": "Reflection period", - "consent_yes": "yes, consent is given", - "consent_no": "no, consent is not given", - "available_yes": "yes, available", - "available_no": "no, not available", - "client_canceled_NAT": "Client discontinued NAT", - "after_meeting": "after session", - "without_giving_reasons": "without giving reasons", - "with_reasons_given": "with reasons given", - "select_at_least_one_answer": "Please select at least one answer.", - "select_at_least_minimum": "Please select at least %d answers.", - "demographic_information": "Demographic information", - "rhs": "RHS", - "integration_index_label": "Integration index", - "consultation_results": "Counseling results", - "final_interview": "Final interview", - "follow_up_survey": "Follow-up survey", - "questionnaire_title": "Questionnaires", - "client_code_exists": "This client code already exists.", - "load": "Load", - "date_after": "The date must be after", - "date_before": "The date must be before", - "lay": "it.", - "choose_more_elements": "More elements must be selected.", - "please_client_code": "Please enter client code", - "no_profile": "This client is not yet part of the database", - "questionnaires_finished": "All questionnaires have been completed for this client!", - "edit": "Edit", - "save": "Save", - "upload": "Upload", - "download": "Download", - "database": "Database", - "clients": "Clients", - "client": "Client", - "client_code_label": "Client Code", - "questionnaires": "Questionnaires", - "questionnaire": "Questionnaire", - "questionnaire_id": "Questionnaire ID", - "status": "Status", - "header_label": "Header", - "id": "ID", - "value": "Value", - "no_clients": "No clients available.", - "no_questionnaires": "No questionnaires available.", - "no_questions": "No questions available.", - "question": "Question", - "answer": "Answer", - "download_header": "Download header", - "back": "Back", - "export_success_downloads_headers": "Export successful: Downloads/ClientHeaders.xlsx", - "export_failed": "Export failed.", - "error_generic": "Error", - "not_done": "Not Done", - "none": "None", - "done": "Done", - "locked": "Locked", - "start": "Start", - "points": "Points", - "saved_pdf_csv": "PDF and CSV were saved in the \"Downloads\" folder.", - "no_pdf_viewer": "No PDF viewer installed.", - "save_error": "Error while saving: {message}", - "login_required_title": "Login required", - "username_hint": "Username", - "password_hint": "Password", - "login_btn": "Login", - "exit_btn": "Exit", - "please_username_password": "Please enter username and password.", - "download_failed_no_local_db": "Download failed – no local database available", - "download_failed_use_offline": "Download failed – working offline with existing database", - "login_failed_with_reason": "Login failed: {reason}", - "no_header_template_found": "No header template found", - "login_required": "Please log in first", - "session_label": "Session", - "session_dash": "Session: —", - "hours_short": "h", - "minutes_short": "min", - "online": "Online", - "offline": "Offline", - "open_client_via_load": "Please open the client via \"Load\".", - "database_clients_title": "Database – Clients", - "no_clients_available": "No clients available.", - "headers": "Headers", - "no_questions_available": "No questions available.", - "questionnaire_missing_options": "This questionnaire is incomplete (missing answer options). Please contact your administrator.", - "view_missing": "Missing view: %s" -} diff --git a/uploadDeltaTest5.php b/uploadDeltaTest5.php deleted file mode 100644 index 852026d..0000000 --- a/uploadDeltaTest5.php +++ /dev/null @@ -1,356 +0,0 @@ - "Only POST allowed"]); - exit; - } - - // Token from multipart field or Authorization: Bearer - $token = $_POST['token'] ?? null; - if (!$token) { - $auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? ''); - if (stripos($auth, 'Bearer ') === 0) { - $token = substr($auth, 7); - } - } - $tokenRec = $token ? token_get_record($token) : null; - if (!$tokenRec || !empty($tokenRec['temp'])) { - http_response_code(403); - echo json_encode(["error" => "Invalid token"]); - exit; - } - - // Upload: multipart 'file' or raw body - $uploadedFilePath = null; - if (!empty($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) { - $uploadedFilePath = $_FILES['file']['tmp_name']; - } else { - $raw = file_get_contents('php://input'); - if ($raw !== false && strlen($raw) > 0) { - $tmp = tempnam(sys_get_temp_dir(), 'upl_'); - file_put_contents($tmp, $raw); - $uploadedFilePath = $tmp; - } - } - if (!$uploadedFilePath) { - http_response_code(400); - echo json_encode(["error" => "No file sent"]); - exit; - } - - // Decrypt payload with session key derived from token via HKDF - $encData = file_get_contents($uploadedFilePath); - if ($encData === false) { - http_response_code(500); - echo json_encode(["error" => "Failed to read uploaded file"]); - exit; - } - $sessionKey = hkdf_session_key_from_token($token); - $jsonData = null; - try { - $jsonData = aes256_cbc_decrypt_bytes($encData, $sessionKey); - } catch (Throwable $e) { - $maybeJson = trim($encData); - if ($maybeJson === '' || json_decode($maybeJson, true) === null) { - log_msg("Decryption failed and not valid JSON. raw len=" . strlen($encData)); - http_response_code(400); - echo json_encode(["error" => "Invalid encrypted file or not JSON"]); - exit; - } - $jsonData = $maybeJson; - } - - $data = json_decode($jsonData, true); - if (!is_array($data)) { - http_response_code(400); - echo json_encode(["error" => "Invalid JSON"]); - exit; - } - - // RBAC: coaches can only upload for their own clients - $role = $tokenRec['role'] ?? ''; - $entityID = $tokenRec['entityID'] ?? ''; - - [$pdo, $tmpDb, $lockFp] = qdb_open(true); - - try { - $pdo->beginTransaction(); - - if ($role === 'coach' && !empty($data['client'])) { - foreach ($data['client'] as $c) { - if (!isset($c['clientCode'])) continue; - $chk = $pdo->prepare("SELECT coachID FROM client WHERE clientCode = :cc"); - $chk->execute([':cc' => $c['clientCode']]); - $existing = $chk->fetch(PDO::FETCH_ASSOC); - if ($existing && $existing['coachID'] !== $entityID) { - $pdo->rollBack(); - qdb_discard($tmpDb, $lockFp); - http_response_code(403); - echo json_encode(["error" => "Not authorized to modify client " . $c['clientCode']]); - exit; - } - } - } - - // --- Collect affected IDs --- - $questionnaireIds = []; - $clientQuestionPairs = []; - - if (!empty($data['questionnaire'])) { - foreach ($data['questionnaire'] as $q) { - if (!empty($q['questionnaireID'])) $questionnaireIds[$q['questionnaireID']] = true; - } - } - if (!empty($data['question'])) { - foreach ($data['question'] as $q) { - if (!empty($q['questionnaireID'])) $questionnaireIds[$q['questionnaireID']] = true; - } - } - if (!empty($data['completed_questionnaire'])) { - foreach ($data['completed_questionnaire'] as $c) { - if (!empty($c['questionnaireID'])) $questionnaireIds[$c['questionnaireID']] = true; - if (isset($c['clientCode'], $c['questionnaireID'])) { - $cc = $c['clientCode'] ?: 'undefined'; - $qid = $c['questionnaireID'] ?: 'undefined'; - $clientQuestionPairs["$cc|$qid"] = true; - } - } - } - if (!empty($data['client_answer'])) { - foreach ($data['client_answer'] as $a) { - if (!isset($a['clientCode'])) continue; - $cc = $a['clientCode'] ?: 'undefined'; - if (isset($a['questionID']) && is_string($a['questionID'])) { - $parts = explode('-', $a['questionID'], 2); - if ($parts[0] !== '') { - $questionnaireIds[$parts[0]] = true; - $clientQuestionPairs["$cc|{$parts[0]}"] = true; - } - } - } - } - if (!empty($data['client']) && !empty($data['questionnaire'])) { - foreach ($data['client'] as $c) { - if (!isset($c['clientCode'])) continue; - $cc = $c['clientCode'] ?: 'undefined'; - foreach ($data['questionnaire'] as $q) { - if (!isset($q['questionnaireID'])) continue; - $qid = $q['questionnaireID'] ?: 'undefined'; - $clientQuestionPairs["$cc|$qid"] = true; - } - } - } - - // --- Deletions (dependents first) --- - $delAnswersByPairStmt = $pdo->prepare( - "DELETE FROM client_answer - WHERE clientCode = :cc - AND questionID IN (SELECT questionID FROM question WHERE questionnaireID = :qid)" - ); - $delCompletedByPairStmt = $pdo->prepare( - "DELETE FROM completed_questionnaire - WHERE clientCode = :cc AND questionnaireID = :qid" - ); - foreach ($clientQuestionPairs as $pair => $_) { - [$cc, $qid] = explode('|', $pair, 2); - try { - $delAnswersByPairStmt->execute([':cc' => $cc, ':qid' => $qid]); - $delCompletedByPairStmt->execute([':cc' => $cc, ':qid' => $qid]); - log_msg("Deleted answers & completed for client='$cc' questionnaire='$qid'"); - } catch (Exception $e) { - log_msg("Delete error for pair $pair: " . $e->getMessage()); - } - } - - if (!empty($questionnaireIds)) { - $delAOStmt = $pdo->prepare( - "DELETE FROM answer_option WHERE questionID IN (SELECT questionID FROM question WHERE questionnaireID = :qid)" - ); - $delQTransStmt = $pdo->prepare( - "DELETE FROM question_translation WHERE questionID IN (SELECT questionID FROM question WHERE questionnaireID = :qid)" - ); - $delQuestionsStmt = $pdo->prepare("DELETE FROM question WHERE questionnaireID = :qid"); - foreach ($questionnaireIds as $qid => $_) { - try { - $delAOStmt->execute([':qid' => $qid]); - $delQTransStmt->execute([':qid' => $qid]); - $delQuestionsStmt->execute([':qid' => $qid]); - log_msg("Deleted questions/options/translations for questionnaireID='$qid'"); - } catch (Exception $e) { - log_msg("Delete questions error for $qid: " . $e->getMessage()); - } - } - } - - // --- Inserts/Upserts --- - if (!empty($data['client'])) { - $stmt = $pdo->prepare( - "INSERT OR REPLACE INTO client (clientCode, coachID) VALUES (:cc, :cid)" - ); - foreach ($data['client'] as $c) { - if (!isset($c['clientCode'])) continue; - $cc = $c['clientCode'] ?: 'undefined'; - $cid = $c['coachID'] ?? $entityID; - $stmt->execute([':cc' => $cc, ':cid' => $cid]); - } - } - - if (!empty($data['questionnaire'])) { - $stmt = $pdo->prepare( - "INSERT OR REPLACE INTO questionnaire (questionnaireID, name, version, state) - VALUES (:qid, :name, :ver, :state)" - ); - foreach ($data['questionnaire'] as $q) { - if (!isset($q['questionnaireID'])) continue; - $stmt->execute([ - ':qid' => $q['questionnaireID'] ?: 'undefined', - ':name' => $q['name'] ?? '', - ':ver' => $q['version'] ?? '', - ':state' => $q['state'] ?? '', - ]); - } - } - - if (!empty($data['question'])) { - $stmt = $pdo->prepare( - "INSERT OR IGNORE INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired) - VALUES (:qid, :qqid, :dt, :t, :oi, :ir)" - ); - foreach ($data['question'] as $q) { - if (!isset($q['questionID'])) continue; - $stmt->execute([ - ':qid' => $q['questionID'] ?: 'undefined', - ':qqid' => $q['questionnaireID'] ?? 'undefined', - ':dt' => $q['defaultText'] ?? '', - ':t' => $q['type'] ?? '', - ':oi' => (int)($q['orderIndex'] ?? 0), - ':ir' => (int)($q['isRequired'] ?? 0), - ]); - } - } - - if (!empty($data['answer_option'])) { - $stmt = $pdo->prepare( - "INSERT OR IGNORE INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex) - VALUES (:aoid, :qid, :dt, :pts, :oi)" - ); - foreach ($data['answer_option'] as $ao) { - if (!isset($ao['answerOptionID'])) continue; - $stmt->execute([ - ':aoid' => $ao['answerOptionID'], - ':qid' => $ao['questionID'] ?? 'undefined', - ':dt' => $ao['defaultText'] ?? '', - ':pts' => (int)($ao['points'] ?? 0), - ':oi' => (int)($ao['orderIndex'] ?? 0), - ]); - } - } - - if (!empty($data['client_answer'])) { - $stmt = $pdo->prepare( - "INSERT OR REPLACE INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt) - VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)" - ); - foreach ($data['client_answer'] as $a) { - if (!isset($a['clientCode'], $a['questionID'])) continue; - $stmt->execute([ - ':cc' => $a['clientCode'] ?: 'undefined', - ':qid' => $a['questionID'] ?: 'undefined', - ':aoid' => $a['answerOptionID'] ?? null, - ':ftv' => $a['freeTextValue'] ?? null, - ':nv' => isset($a['numericValue']) ? (float)$a['numericValue'] : null, - ':at' => isset($a['answeredAt']) ? (int)$a['answeredAt'] : null, - ]); - } - } - - if (!empty($data['completed_questionnaire'])) { - $stmt = $pdo->prepare( - "INSERT OR REPLACE INTO completed_questionnaire - (clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, sumPoints) - VALUES (:cc, :qid, :abc, :st, :sa, :ca, :sp)" - ); - foreach ($data['completed_questionnaire'] as $c) { - if (!isset($c['clientCode'], $c['questionnaireID'])) continue; - $stmt->execute([ - ':cc' => $c['clientCode'] ?: 'undefined', - ':qid' => $c['questionnaireID'] ?: 'undefined', - ':abc' => $c['assignedByCoach'] ?? null, - ':st' => $c['status'] ?? '', - ':sa' => isset($c['startedAt']) ? (int)$c['startedAt'] : null, - ':ca' => isset($c['completedAt']) ? (int)$c['completedAt'] : null, - ':sp' => isset($c['sumPoints']) ? (int)$c['sumPoints'] : null, - ]); - } - } - - if (!empty($data['question_translation'])) { - $stmt = $pdo->prepare( - "INSERT OR REPLACE INTO question_translation (questionID, languageCode, text) - VALUES (:qid, :lc, :txt)" - ); - foreach ($data['question_translation'] as $qt) { - if (!isset($qt['questionID'], $qt['languageCode'])) continue; - $stmt->execute([ - ':qid' => $qt['questionID'], - ':lc' => $qt['languageCode'], - ':txt' => $qt['text'] ?? '', - ]); - } - } - - if (!empty($data['answer_option_translation'])) { - $stmt = $pdo->prepare( - "INSERT OR REPLACE INTO answer_option_translation (answerOptionID, languageCode, text) - VALUES (:aoid, :lc, :txt)" - ); - foreach ($data['answer_option_translation'] as $aot) { - if (!isset($aot['answerOptionID'], $aot['languageCode'])) continue; - $stmt->execute([ - ':aoid' => $aot['answerOptionID'], - ':lc' => $aot['languageCode'], - ':txt' => $aot['text'] ?? '', - ]); - } - } - - $pdo->commit(); - $pdo = null; - - qdb_save($tmpDb, $lockFp); - @unlink($uploadedFilePath); - - echo json_encode(["success" => true, "message" => "Delta applied and DB saved"]); - exit; - - } catch (Throwable $inner) { - qdb_discard($tmpDb, $lockFp); - @unlink($uploadedFilePath ?? ''); - log_msg("Inner exception: " . $inner->getMessage()); - http_response_code(500); - error_log($inner->getMessage()); - echo json_encode(["error" => "Save error"]); - exit; - } - -} catch (Throwable $e) { - log_msg("Top-level exception: " . $e->getMessage()); - http_response_code(500); - error_log($e->getMessage()); - echo json_encode(["error" => "Server error"]); - exit; -} diff --git a/users.json b/users.json deleted file mode 100644 index 16fa4ff..0000000 --- a/users.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "user01": { - "hash": "$2y$10$fcB5mBc.RDeCHyY69RScZOpFL/IjY.wFzx1DNzKI7F6kJhd9rwQfu", - "changedAt": 1760610683 - }, - "user02": { - "hash": "$2y$10$fGDcFLBgL3oypyjkwVna/OhQ5Be6UFH6hNGdupeNfJMASgrFK7h0K", - "changedAt": 1760611508 - }, - "Tmp_Daniel": { - "hash": "$2y$10$NRdP4/vkDQEM4fD70xGqdO1dgujOBATcA00Zj2aIaH0hoQV5Rp8ri", - "changedAt": 1760612940 - }, - "Tmp_Liliana": { - "hash": "$2y$10$4Gui323PXRnQ9Ix4WSrefOEr2HQwfbr6TNVaa7qVE2q59ZwBpnisa", - "changedAt": 1760971357 - }, - "Tmp_Johanna": { - "hash": "$2y$10$B4eNyer1anHYMNjpNliIW.54sK8z1cYyQEHjmHloatUnmeilchjva", - "changedAt": 1760972135 - }, - "Danny": { - "hash": "$2y$10$JreuWqvh1VO3fH2YhSpqveDpSSgVPUeNW91ySSzxeQ0o/OgNe1gGS", - "changedAt": 1771865346 - }, - "Extra1_tmp": { - "hash": "$2y$10$zUtdsv1N2Uv/7XZT4wr4lOTnJEWFF74EJp2/kgq6GHpO8dguMQify", - "changedAt": 1771865748 - }, - "User11": { - "hash": "$2y$10$ql0aI9C.1nzkE33Oxy8T3.dE0jF0y42fe4x8YWyqFm.SwlUBoqv22", - "changedAt": 1772001151 - }, - "User10": { - "hash": "$2y$10$5HoG6T6UA9CFd2q1VGLiE.3EbwNyyVR8kSuTZWxJ5run8KvHgrhhK", - "changedAt": 1772110997 - }, - "User2": { - "hash": "$2y$10$/KxLlL9ZdHnGYL1y4IEIfOnWL.bd2HgPVfk.n5V0Sc.3LzDwmeWl6", - "changedAt": 1772111983 - }, - "User3": { - "hash": "$2y$10$GMtUWbW.bczjc43dpf539OE7rFgwL08QV1qnjFlyDh5mF/Ss4atom", - "changedAt": 1772112290 - }, - "User7": { - "hash": "$2y$10$AfJp8go8Vt5J0.46SqYmtOPrrp1CqkWw1pmkOGI6xZ5MgqHTmwIga", - "changedAt": 1772283082 - } -} \ No newline at end of file diff --git a/valid_tokens.txt b/valid_tokens.txt deleted file mode 100644 index a00589e..0000000 --- a/valid_tokens.txt +++ /dev/null @@ -1,24 +0,0 @@ -e27b08a83d8d034746ad3f860758aa20e3c302deef5da0df29a6ab93f71dda90 -4215cb8aa644120fce31c8bd9ff3cb3e100b6ae2f19c83bba9e1c6a87e7f4e99 -0441863e59bbc0bef45c1377ae31a7155c249cf298981dad3aaa7d9389635d95 -d9813ed055fd974e92c92ffd2b9c64b86a572f4dfe0a075f6e68e4d057ae9629 -b2277c780fb2c8ed218173c76bd7f36dce48bfb284862fdd04732fe84a9d3437 -ac2d9596f485f6f15051f267299c24aac60bd743f020c60f250d5086b1ae6dba -3ae1ea99a01abedd3e2dfe6e2a4e8095a127353bf738ffb7baa0ca14b49b3ee6 -324478b89a99a8b6987131b26d729364ef7fa7cadb51634fae160b12b052bd49 -40032db80223b526499a7553ac4b58ecb0276b4aac8023444de1c674c07cc394 -70417759cda569e3c11b6a443431e536d7eda73268e338ab40f7500d056083bb -f067dd4ca164b0fa66a87b299308390da0913da1b16cddad73c1b322272379b6 -3b381d3013a68896662cad27196e77e4dd3dd0117a9ddf08922dbdff807d4fb8 -33cf888d8a443dd3055650a8962293e7251620d9a1a8a64a6a224611bc41d3b8 -22f588244d7635bc6faf329a6291e91b3dd1ca301e2f2ec084a921e18a757ca4 -26c6ef3ebfbfd8f267f96a60bc4913504ade4b3edd0404668a2cf7af0dcb99e9 -fadf7a06ead066c607d5714546b6f4827bd21b3a4542e68386fb34ace4bcaa53 -9fcea9c8d98fa09274dbbf6741bea50ab174a01b9b5d26138f933adf1f29957c -20fbfb3a6f248a24ab33769a5e5de06732f402e0c9b9f709d17cc3b442c3b1ba -e88d0746883123156433e55a5e07044e253f3429472df78e5d61f43c2c40e760 -4cc8d74f0fe3d655e16c337612d02a68208620169c6d7ca587a25fddce61c004 -2f5d7b59ccb55b21d4a5b7d7e03c9966df5c1377f447098ab398e9b789a6a188 -f1f2ba5d1a6e898688658006c8c42c0f6cb8a6e214f08d016cfc1a2a72af1e8e -02ded9879607050fc2531d8fb9265faadcc4c0a6ffced7a82eb919a0354f9c9c -4923c95ed744e8eee5d4f215b16c2a1141ec49bca7f616651f124865e06e4e1b