Symfony 8 SPA with Doctrine ORM, Symfony Security, vanilla JS frontend. Migrated from plain PHP (delight-im/auth + raw SQL) to full Symfony stack. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
294 lines
13 KiB
PHP
294 lines
13 KiB
PHP
<?php
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', 0);
|
||
ini_set('log_errors', 1);
|
||
ini_set('error_log', '/tmp/zt.log');
|
||
set_exception_handler(function(\Throwable $e) {
|
||
http_response_code(500);
|
||
echo json_encode(['error' => $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);
|