$e->getMessage(), 'file' => basename($e->getFile()), 'line' => $e->getLine()]); exit; }); require_once __DIR__ . '/include/db.php'; require_once __DIR__ . '/include/mailer.php'; header('Content-Type: application/json; charset=utf-8'); // CORS für gleiche Domain nicht nötig; bei separaten Dev-Domains ggf. ergänzen $method = $_SERVER['REQUEST_METHOD']; $segments = explode('/', trim($_GET['_path'] ?? '', '/')); $resource = $segments[0] ?? ''; $resourceId = isset($segments[1]) && $segments[1] !== '' ? (int)$segments[1] : null; $body = json_decode(file_get_contents('php://input'), true) ?? []; function json_out(array $data, int $status = 200): never { http_response_code($status); echo json_encode($data); exit; } function require_auth(\Delight\Auth\Auth $auth): int { if (!$auth->isLoggedIn()) { json_out(['error' => 'Unauthorized'], 401); } return $auth->getUserId(); } // ── POST /api/login ────────────────────────────────────────────────────────── if ($method === 'POST' && $resource === 'login') { $email = trim($body['email'] ?? ''); $pass = $body['password'] ?? ''; try { $auth->login($email, $pass, 86400); json_out(['ok' => true, 'email' => $auth->getEmail()]); } catch (\Delight\Auth\InvalidEmailException | \Delight\Auth\InvalidPasswordException | \Delight\Auth\UserNotFound $e) { json_out(['error' => 'Falsche E-Mail oder Passwort'], 401); } catch (\Delight\Auth\TooManyRequestsException $e) { json_out(['error' => 'Zu viele Versuche, bitte warten'], 429); } } // ── POST /api/logout ───────────────────────────────────────────────────────── if ($method === 'POST' && $resource === 'logout') { $auth->logOut(); json_out(['ok' => true]); } // ── GET /api/me ─────────────────────────────────────────────────────────────── if ($method === 'GET' && $resource === 'me') { if ($auth->isLoggedIn()) { $name = $conn->fetchOne('SELECT username FROM users WHERE id = ?', [$auth->getUserId()]); json_out(['ok' => true, 'email' => $auth->getEmail(), 'id' => $auth->getUserId(), 'name' => $name ?: '']); } json_out(['ok' => false], 401); } // ── PATCH /api/me ───────────────────────────────────────────────────────────── if ($method === 'PATCH' && $resource === 'me') { $uid = require_auth($auth); $name = trim($body['name'] ?? ''); if (!$name) json_out(['error' => 'Name fehlt'], 400); $conn->executeStatement('UPDATE users SET username = ? WHERE id = ?', [$name, $uid]); json_out(['ok' => true, 'name' => $name]); } // ── POST /api/register ──────────────────────────────────────────────────────── if ($method === 'POST' && $resource === 'register') { $email = trim($body['email'] ?? ''); $pass = $body['password'] ?? ''; $token = trim($body['token'] ?? ''); if (!$email || !$pass || !$token) { json_out(['error' => 'Fehlende Felder'], 400); } // Token validieren $invite = $conn->fetchAssociative( 'SELECT * FROM invites WHERE token = ? AND used_by IS NULL AND expires_at > NOW()', [$token] ); if (!$invite) { json_out(['error' => 'Ungültiger oder abgelaufener Einladungslink'], 400); } $name = trim($body['name'] ?? ''); try { $userId = $auth->register($email, $pass); } catch (\Delight\Auth\InvalidEmailException $e) { json_out(['error' => 'Ungültige E-Mail'], 400); } catch (\Delight\Auth\InvalidPasswordException $e) { json_out(['error' => 'Passwort zu kurz (min. 8 Zeichen)'], 400); } catch (\Delight\Auth\UserAlreadyExistsException $e) { json_out(['error' => 'E-Mail bereits registriert'], 409); } // Token als verwendet markieren $conn->executeStatement( 'UPDATE invites SET used_by = ?, used_at = NOW() WHERE token = ?', [$userId, $token] ); if ($name) { $conn->executeStatement('UPDATE users SET username = ? WHERE id = ?', [$name, $userId]); } // Direkt einloggen $auth->login($email, $pass, 86400); json_out(['ok' => true, 'email' => $auth->getEmail(), 'name' => $name]); } // ── POST /api/reset-request ────────────────────────────────────────────────── if ($method === 'POST' && $resource === 'reset-request') { $email = trim($body['email'] ?? ''); if (!$email) json_out(['error' => 'E-Mail fehlt'], 400); try { $auth->requestPasswordReset($email, function (string $selector, string $token) use ($email) { $url = APP_URL . '/?reset_selector=' . rawurlencode($selector) . '&reset_token=' . rawurlencode($token); $text = "Hallo,\n\nklicke auf den folgenden Link um dein Passwort zurückzusetzen (gültig 24h):\n\n$url\n\nFalls du das nicht angefragt hast, ignoriere diese E-Mail."; sendMail($email, 'Durchzieh-Dienst – Passwort zurücksetzen', $text); }); } catch (\Delight\Auth\InvalidEmailException $e) { // Keinen Hinweis geben ob die E-Mail existiert } catch (\Exception $e) { json_out(['error' => 'Mail konnte nicht gesendet werden'], 500); } json_out(['ok' => true]); // immer ok, auch wenn E-Mail unbekannt } // ── POST /api/reset-password ────────────────────────────────────────────────── if ($method === 'POST' && $resource === 'reset-password') { $selector = $body['selector'] ?? ''; $token = $body['token'] ?? ''; $pass = $body['password'] ?? ''; if (!$selector || !$token || !$pass) json_out(['error' => 'Fehlende Felder'], 400); try { $auth->resetPassword($selector, $token, $pass); json_out(['ok' => true]); } catch (\Delight\Auth\InvalidSelectorTokenPairException $e) { json_out(['error' => 'Ungültiger oder abgelaufener Reset-Link'], 400); } catch (\Delight\Auth\TokenExpiredException $e) { json_out(['error' => 'Reset-Link abgelaufen, bitte neu anfordern'], 400); } catch (\Delight\Auth\InvalidPasswordException $e) { json_out(['error' => 'Passwort zu kurz (min. 8 Zeichen)'], 400); } } // ── POST /api/change-password ───────────────────────────────────────────────── if ($method === 'POST' && $resource === 'change-password') { $uid = require_auth($auth); $oldPass = $body['old_password'] ?? ''; $newPass = $body['new_password'] ?? ''; if (!$oldPass || !$newPass) json_out(['error' => 'Fehlende Felder'], 400); try { $auth->changePassword($oldPass, $newPass); json_out(['ok' => true]); } catch (\Delight\Auth\NotLoggedInException $e) { json_out(['error' => 'Nicht eingeloggt'], 401); } catch (\Delight\Auth\InvalidPasswordException $e) { json_out(['error' => 'Neues Passwort zu kurz (min. 8 Zeichen)'], 400); } catch (\Delight\Auth\TooManyRequestsException $e) { json_out(['error' => 'Zu viele Versuche'], 429); } } // ── POST /api/invite ───────────────────────────────────────────────────────── if ($method === 'POST' && $resource === 'invite') { $uid = require_auth($auth); $token = bin2hex(random_bytes(32)); $note = trim($body['note'] ?? ''); $conn->executeStatement( 'INSERT INTO invites (token, note, created_by, expires_at) VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))', [$token, $note ?: null, $uid] ); json_out(['url' => APP_URL . '/?invite=' . $token]); } // ── GET /api/invites ────────────────────────────────────────────────────────── if ($method === 'GET' && $resource === 'invites') { $uid = require_auth($auth); $rows = $conn->fetchAllAssociative( 'SELECT i.token, i.created_at, i.expires_at, i.used_at, i.note, u.email AS used_by_email FROM invites i LEFT JOIN users u ON u.id = i.used_by WHERE i.created_by = ? ORDER BY i.created_at DESC', [$uid] ); $result = array_map(function ($r) { if ($r['used_by_email'] !== null) { $status = 'used'; } elseif (strtotime($r['expires_at']) < time()) { $status = 'expired'; } else { $status = 'pending'; } return [ 'url' => $status === 'pending' ? APP_URL . '/?invite=' . $r['token'] : null, 'created_at' => $r['created_at'], 'expires_at' => $r['expires_at'], 'used_at' => $r['used_at'], 'note' => $r['note'], 'used_by_email' => $r['used_by_email'], 'status' => $status, ]; }, $rows); json_out($result); } // ── GET /api/goals ──────────────────────────────────────────────────────────── if ($method === 'GET' && $resource === 'goals') { $uid = require_auth($auth); $rows = $conn->fetchAllAssociative( 'SELECT id, name, unit, daily, days, start, sets FROM goals WHERE user_id = ? ORDER BY created_at ASC', [$uid] ); $goals = array_map(function ($r) { $r['sets'] = json_decode($r['sets'], true) ?: []; $r['id'] = (string)$r['id']; return $r; }, $rows); json_out($goals); } // ── POST /api/goals ─────────────────────────────────────────────────────────── if ($method === 'POST' && $resource === 'goals') { $uid = require_auth($auth); $name = trim($body['name'] ?? ''); $unit = trim($body['unit'] ?? 'Stück'); $daily = (float)($body['daily'] ?? 1); $days = (int) ($body['days'] ?? 30); try { $start = (new DateTime($body['start'] ?? 'now'))->format('Y-m-d H:i:s'); } catch (\Exception $e) { $start = date('Y-m-d H:i:s'); } $setsJson = isset($body['sets']) && is_array($body['sets']) ? json_encode((object)$body['sets']) : '{}'; if (!$name) json_out(['error' => 'Name fehlt'], 400); $conn->executeStatement( 'INSERT INTO goals (user_id, name, unit, daily, days, start, sets) VALUES (?, ?, ?, ?, ?, ?, ?)', [$uid, $name, $unit, $daily, $days, $start, $setsJson] ); $id = (string)$conn->lastInsertId(); json_out(['id' => $id, 'name' => $name, 'unit' => $unit, 'daily' => $daily, 'days' => $days, 'start' => $start, 'sets' => json_decode($setsJson, true)]); } // ── PATCH /api/goals/{id} ───────────────────────────────────────────────────── if ($method === 'PATCH' && $resource === 'goals' && $resourceId !== null) { $uid = require_auth($auth); // Sicherstellen dass das Ziel dem User gehört $existing = $conn->fetchOne('SELECT id FROM goals WHERE id = ? AND user_id = ?', [$resourceId, $uid]); if (!$existing) json_out(['error' => 'Nicht gefunden'], 404); $fields = []; $params = []; foreach (['name', 'unit'] as $f) { if (isset($body[$f])) { $fields[] = "$f = ?"; $params[] = (string)$body[$f]; } } foreach (['daily'] as $f) { if (isset($body[$f])) { $fields[] = "$f = ?"; $params[] = (float)$body[$f]; } } foreach (['days'] as $f) { if (isset($body[$f])) { $fields[] = "$f = ?"; $params[] = (int)$body[$f]; } } if (isset($body['sets'])) { $fields[] = 'sets = ?'; $params[] = json_encode($body['sets']); } if (!$fields) json_out(['ok' => true]); $params[] = $resourceId; $params[] = $uid; $conn->executeStatement('UPDATE goals SET ' . implode(', ', $fields) . ' WHERE id = ? AND user_id = ?', $params); json_out(['ok' => true]); } // ── DELETE /api/goals/{id} ──────────────────────────────────────────────────── if ($method === 'DELETE' && $resource === 'goals' && $resourceId !== null) { $uid = require_auth($auth); $conn->executeStatement('DELETE FROM goals WHERE id = ? AND user_id = ?', [$resourceId, $uid]); json_out(['ok' => true]); } json_out(['error' => 'Not found'], 404);