dudi/api.php

295 lines
13 KiB
PHP
Raw Normal View History

<?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);