feat: PHP API foundation - router, config, helpers, SM-2 algorithm
Dieser Commit ist enthalten in:
Ursprung
9f942a0e56
Commit
581888e142
4 geänderte Dateien mit 152 neuen und 0 gelöschten Zeilen
30
edu/api/config.php
Normale Datei
30
edu/api/config.php
Normale Datei
|
|
@ -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
41
edu/api/helpers.php
Normale Datei
|
|
@ -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
42
edu/api/index.php
Normale Datei
|
|
@ -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
39
edu/api/sm2.php
Normale Datei
|
|
@ -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
|
||||||
|
];
|
||||||
|
}
|
||||||
Laden …
Tabelle hinzufügen
Einen Link hinzufügen
In neuem Issue referenzieren