feat: PHP API foundation - router, config, helpers, SM-2 algorithm

Dieser Commit ist enthalten in:
hafroese 2026-04-02 23:05:27 +02:00
Ursprung 9f942a0e56
Commit 581888e142
4 geänderte Dateien mit 152 neuen und 0 gelöschten Zeilen

30
edu/api/config.php Normale Datei
Datei anzeigen

@ -0,0 +1,30 @@
<?php
$db_host = getenv('EDU_DB_HOST') ?: 'postgres';
$db_port = getenv('EDU_DB_PORT') ?: '5432';
$db_name = getenv('EDU_DB_NAME') ?: 'edu';
$db_user = getenv('EDU_DB_USER') ?: 'postgres';
$db_pass = getenv('EDU_DB_PASS') ?: '';
try {
$pdo = new PDO(
"pgsql:host={$db_host};port={$db_port};dbname={$db_name}",
$db_user, $db_pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Datenbankverbindung fehlgeschlagen']);
exit;
}
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
define('CONTENT_DIR', __DIR__ . '/../content');

41
edu/api/helpers.php Normale Datei
Datei anzeigen

@ -0,0 +1,41 @@
<?php
header('Content-Type: application/json; charset=utf-8');
function json_ok($data) {
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function json_error($message, $code = 400) {
http_response_code($code);
echo json_encode(['error' => $message], JSON_UNESCAPED_UNICODE);
exit;
}
function require_auth() {
if (empty($_SESSION['user_id'])) {
json_error('Nicht angemeldet', 401);
}
return $_SESSION['user_id'];
}
function require_admin() {
require_auth();
if (empty($_SESSION['is_admin'])) {
json_error('Keine Admin-Berechtigung', 403);
}
return $_SESSION['user_id'];
}
function get_method() {
return $_SERVER['REQUEST_METHOD'];
}
function get_json_body() {
$body = file_get_contents('php://input');
return json_decode($body, true) ?: [];
}
function get_param($name, $default = null) {
return $_GET[$name] ?? $default;
}

42
edu/api/index.php Normale Datei
Datei anzeigen

@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
// Parse: /api/decks/omnis-commands -> ['decks', 'omnis-commands']
$request_uri = $_SERVER['REQUEST_URI'];
$path = parse_url($request_uri, PHP_URL_PATH);
$path = preg_replace('#^/api/?#', '', $path);
$path = trim($path, '/');
$segments = $path ? explode('/', $path) : [];
$resource = $segments[0] ?? '';
switch ($resource) {
case 'login':
case 'logout':
case 'me':
require __DIR__ . '/auth.php';
break;
case 'decks':
require __DIR__ . '/decks.php';
break;
case 'cards':
require __DIR__ . '/cards.php';
break;
case 'tutorials':
require __DIR__ . '/tutorials.php';
break;
case 'quiz':
require __DIR__ . '/quiz.php';
break;
case 'cheatsheets':
require __DIR__ . '/cheatsheets.php';
break;
case 'dashboard':
require __DIR__ . '/dashboard.php';
break;
case 'admin':
require __DIR__ . '/admin.php';
break;
default:
json_error('Unbekannter Endpunkt', 404);
}

39
edu/api/sm2.php Normale Datei
Datei anzeigen

@ -0,0 +1,39 @@
<?php
/**
* SM-2 Spaced Repetition Algorithm
* Ratings: easy=5, medium=3, hard=2, wrong=0
*/
function sm2_calculate($rating_name, $current_ease, $current_interval, $current_reps) {
$rating_map = ['wrong' => 0, 'hard' => 2, 'medium' => 3, 'easy' => 5];
$q = $rating_map[$rating_name] ?? 3;
$ease = $current_ease;
$interval = $current_interval;
$reps = $current_reps;
if ($q < 3) {
$reps = 0;
$interval = 1;
} else {
if ($reps === 0) {
$interval = 1;
} elseif ($reps === 1) {
$interval = 3;
} else {
$interval = (int) round($interval * $ease);
}
$reps++;
}
$ease = $ease + 0.1 - (5 - $q) * (0.08 + (5 - $q) * 0.02);
if ($ease < 1.3) $ease = 1.3;
$next_review = date('Y-m-d', strtotime("+{$interval} days"));
return [
'ease_factor' => round($ease, 2),
'interval_days' => $interval,
'repetitions' => $reps,
'next_review' => $next_review
];
}