295 lines
13 KiB
PHP
295 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);
|