new system prototype
This commit is contained in:
204
common.php
204
common.php
@ -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 ---
|
||||
|
||||
Reference in New Issue
Block a user