new system prototype

This commit is contained in:
2026-04-15 10:19:42 +02:00
parent e805f225bc
commit 034b108c7e
80 changed files with 12212 additions and 890 deletions

View File

@ -2,20 +2,62 @@
// /var/www/html/common.php
// Gemeinsame Helfer für Token-Validierung, Key-Derivation (HKDF) und AES (CBC, IV vorangestellt).
/**
* Load KEY=value pairs from .env in the project root.
* Does not override variables already set in the real environment.
*/
function qdb_load_dotenv(string $path): void {
if (!is_readable($path)) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
$eq = strpos($line, '=');
if ($eq === false) {
continue;
}
$name = trim(substr($line, 0, $eq));
$value = trim(substr($line, $eq + 1));
if ($name === '') {
continue;
}
$len = strlen($value);
if ($len >= 2) {
$q = $value[0];
if (($q === '"' && $value[$len - 1] === '"') || ($q === "'" && $value[$len - 1] === "'")) {
$value = substr($value, 1, -1);
}
}
$existing = getenv($name);
if ($existing !== false && $existing !== '') {
continue;
}
putenv("$name=$value");
$_ENV[$name] = $value;
}
}
qdb_load_dotenv(__DIR__ . '/.env');
// --- MASTER-KEY (Server-seitig zum Speichern der DB) ---
// Per ENV 'QDB_MASTER_KEY' (Base64- oder ASCII-String) oder Fallback auf bisherigen Wert.
function get_master_key_bytes(): string {
$env = getenv('QDB_MASTER_KEY');
if ($env && $env !== '') {
// Versuche Base64 zu dekodieren, sonst als ASCII nehmen
$b = base64_decode($env, true);
if ($b !== false && strlen($b) > 0) {
return str_pad(substr($b, 0, 32), 32, "\0");
}
return str_pad(substr($env, 0, 32), 32, "\0");
if (!$env || $env === '') {
throw new RuntimeException('QDB_MASTER_KEY environment variable is not set. Cannot proceed without a master key.');
}
// Fallback: alter Key aus bestehender Installation (32 ASCII-Bytes)
return "12345678901234567890123456789012";
$b = base64_decode($env, true);
if ($b !== false && strlen($b) > 0) {
return str_pad(substr($b, 0, 32), 32, "\0");
}
return str_pad(substr($env, 0, 32), 32, "\0");
}
// --- HKDF (SHA-256) über Token -> Session-Key (32 Byte) ---
@ -40,36 +82,124 @@ function hkdf_session_key_from_token(string $tokenHex, string $info = 'qdb-aes',
return substr($okm, 0, $len);
}
// --- Token-Storage (mit Ablauf) ---
function tokens_file_path(): string {
return __DIR__ . '/tokens.jsonl';
}
function token_add(string $token, int $ttlSeconds = 86400): void {
$rec = [
'token' => $token,
'created' => time(),
'exp' => time() + $ttlSeconds,
];
file_put_contents(tokens_file_path(), json_encode($rec) . PHP_EOL, FILE_APPEND | LOCK_EX);
// optional weiterhin für Alt-Skripte:
file_put_contents(__DIR__ . '/valid_tokens.txt', $token . PHP_EOL, FILE_APPEND | LOCK_EX);
}
function token_is_valid(string $token): bool {
$f = tokens_file_path();
if (!file_exists($f)) return false;
$h = fopen($f, 'r');
if (!$h) return false;
// --- Token-Storage (DB-backed via session table) ---
function token_add(string $token, int $ttlSeconds = 86400, array $extra = []): void {
require_once __DIR__ . '/db_init.php';
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
$now = time();
$valid = false;
while (($line = fgets($h)) !== false) {
$j = json_decode($line, true);
if (!is_array($j)) continue;
if (($j['token'] ?? '') === $token && ($j['exp'] ?? 0) >= $now) {
$valid = true; break;
}
$pdo->prepare(
"INSERT INTO session (token, userID, role, entityID, createdAt, expiresAt, temp)
VALUES (:t, :uid, :role, :eid, :ca, :ea, :tmp)"
)->execute([
':t' => $token,
':uid' => $extra['userID'] ?? '',
':role' => $extra['role'] ?? '',
':eid' => $extra['entityID'] ?? '',
':ca' => $now,
':ea' => $now + $ttlSeconds,
':tmp' => (int)(!empty($extra['temp'])),
]);
// Opportunistic cleanup: remove expired sessions
$pdo->prepare("DELETE FROM session WHERE expiresAt < :now")->execute([':now' => $now]);
qdb_save($tmpDb, $lockFp);
}
function token_get_record(string $token): ?array {
require_once __DIR__ . '/db_init.php';
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$stmt = $pdo->prepare("SELECT * FROM session WHERE token = :t AND expiresAt >= :now");
$stmt->execute([':t' => $token, ':now' => time()]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
qdb_discard($tmpDb, $lockFp);
if (!$row) return null;
// Map DB columns to the keys the rest of the codebase expects
return [
'token' => $row['token'],
'role' => $row['role'],
'entityID' => $row['entityID'],
'userID' => $row['userID'],
'created' => (int)$row['createdAt'],
'exp' => (int)$row['expiresAt'],
'temp' => (int)$row['temp'],
];
}
function token_revoke(string $token): void {
require_once __DIR__ . '/db_init.php';
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
$pdo->prepare("DELETE FROM session WHERE token = :t")->execute([':t' => $token]);
qdb_save($tmpDb, $lockFp);
}
// --- Auth helpers for endpoints ---
function get_bearer_token(): ?string {
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? '');
if (stripos($auth, 'Bearer ') === 0) {
return substr($auth, 7);
}
return null;
}
function require_valid_token(): array {
$token = get_bearer_token();
if (!$token) {
http_response_code(401);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(["error" => "Missing Bearer token"]);
exit;
}
$rec = token_get_record($token);
if (!$rec) {
http_response_code(403);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(["error" => "Invalid or expired token"]);
exit;
}
if (!empty($rec['temp'])) {
http_response_code(403);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(["error" => "Password change required before access"]);
exit;
}
$rec['_token'] = $token;
return $rec;
}
function require_role(array $allowed, array $tokenRecord): void {
$role = $tokenRecord['role'] ?? '';
if (!in_array($role, $allowed, true)) {
http_response_code(403);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(["error" => "Insufficient permissions"]);
exit;
}
}
/**
* Build a SQL WHERE clause fragment that restricts client visibility by role.
* Returns [string $clause, array $params].
* $clientAlias is the table alias or name that has a coachID column (e.g. 'client').
*/
function rbac_client_filter(array $tokenRecord, string $clientAlias = 'client'): array {
$role = $tokenRecord['role'] ?? '';
$entityID = $tokenRecord['entityID'] ?? '';
switch ($role) {
case 'admin':
return ['1=1', []];
case 'supervisor':
return [
"$clientAlias.coachID IN (SELECT coachID FROM coach WHERE supervisorID = :rbac_eid)",
[':rbac_eid' => $entityID]
];
case 'coach':
return [
"$clientAlias.coachID = :rbac_eid",
[':rbac_eid' => $entityID]
];
default:
return ['0=1', []];
}
fclose($h);
return $valid;
}
// --- AES-256-CBC: IV(16) + CIPHERTEXT ---