117 lines
6.1 KiB
PHP
117 lines
6.1 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once dirname(__DIR__, 3) . '/backend/JwtToken.php';
|
|
require_once dirname(__DIR__, 3) . '/backend/ApiAuth.php';
|
|
require_once __DIR__ . '/_bootstrap.php';
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
|
if (!in_array($method, ['GET', 'POST'], true)) {
|
|
header('Allow: GET, POST');
|
|
api_json(['error' => 'method_not_allowed'], 405);
|
|
}
|
|
$auth = ApiAuth::bearerClaims();
|
|
|
|
function meal_api_number($value, string $field, float $max): float
|
|
{
|
|
if (!is_int($value) && !is_float($value)) api_json(['error' => 'invalid_' . $field], 400);
|
|
$number = (float) $value;
|
|
if (!is_finite($number) || $number < 0 || $number > $max) api_json(['error' => 'invalid_' . $field], 400);
|
|
return $number;
|
|
}
|
|
|
|
function meal_api_date(string $value): ?DateTimeImmutable
|
|
{
|
|
try {
|
|
$date = new DateTimeImmutable($value, new DateTimeZone('UTC'));
|
|
return $date->setTimezone(new DateTimeZone('UTC'));
|
|
} catch (Throwable $exception) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
try {
|
|
$db = Database::getInstance();
|
|
if ($method === 'GET') {
|
|
$fromRaw = $_GET['from'] ?? gmdate('Y-m-d', strtotime('-30 days'));
|
|
$toRaw = $_GET['to'] ?? gmdate('Y-m-d', strtotime('+1 day'));
|
|
if (!is_string($fromRaw) || !is_string($toRaw)) api_json(['error' => 'invalid_date_range'], 400);
|
|
$from = meal_api_date($fromRaw);
|
|
$to = meal_api_date($toRaw);
|
|
if ($from === null || $to === null || $from >= $to || $to->diff($from)->days > 92) api_json(['error' => 'invalid_date_range'], 400);
|
|
$userId = $auth['user_id'];
|
|
$fromSql = $from->format('Y-m-d H:i:s');
|
|
$toSql = $to->format('Y-m-d H:i:s');
|
|
$query = $db->prepare('SELECT client_meal_uuid, name, meal_type, calories, protein_grams, carbohydrate_grams, fat_grams, consumed_at, source, notes, revision, updated_at FROM meal_entries WHERE user_id = ? AND consumed_at >= ? AND consumed_at < ? ORDER BY consumed_at DESC LIMIT 2000');
|
|
$query->bind_param('iss', $userId, $fromSql, $toSql);
|
|
$query->execute();
|
|
$result = $query->get_result();
|
|
$entries = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
foreach (['calories', 'protein_grams', 'carbohydrate_grams', 'fat_grams'] as $key) $row[$key] = (float) $row[$key];
|
|
$row['revision'] = (int) $row['revision'];
|
|
$row['consumed_at'] = gmdate('Y-m-d\\TH:i:s\\Z', strtotime($row['consumed_at'] . ' UTC'));
|
|
$row['updated_at'] = gmdate('Y-m-d\\TH:i:s\\Z', strtotime($row['updated_at'] . ' UTC'));
|
|
$entries[] = $row;
|
|
}
|
|
$query->close();
|
|
header('Cache-Control: no-store');
|
|
api_json(['meals' => $entries]);
|
|
}
|
|
|
|
$rawBody = file_get_contents('php://input') ?: '';
|
|
if (strlen($rawBody) > 16000) api_json(['error' => 'payload_too_large'], 413);
|
|
$body = json_decode($rawBody, true);
|
|
if (!is_array($body)) api_json(['error' => 'invalid_meal_payload'], 400);
|
|
$clientUuid = $body['client_meal_uuid'] ?? null;
|
|
if (!is_string($clientUuid) || !preg_match('/^[0-9a-f-]{36}$/i', $clientUuid)) api_json(['error' => 'invalid_client_meal_uuid'], 400);
|
|
$name = $body['name'] ?? null;
|
|
if (!is_string($name) || trim($name) === '' || mb_strlen($name) > 180) api_json(['error' => 'invalid_name'], 400);
|
|
$mealType = $body['meal_type'] ?? null;
|
|
if (!is_string($mealType) || !in_array($mealType, ['breakfast', 'lunch', 'dinner', 'snack'], true)) api_json(['error' => 'invalid_meal_type'], 400);
|
|
$source = $body['source'] ?? 'manual';
|
|
// Until a server-side vision provider exists, clients cannot label guesses as AI analysis.
|
|
if ($source !== 'manual') api_json(['error' => 'unsupported_meal_source'], 400);
|
|
$calories = meal_api_number($body['calories'] ?? null, 'calories', 10000);
|
|
$protein = meal_api_number($body['protein_grams'] ?? 0, 'protein_grams', 1000);
|
|
$carbs = meal_api_number($body['carbohydrate_grams'] ?? 0, 'carbohydrate_grams', 1000);
|
|
$fat = meal_api_number($body['fat_grams'] ?? 0, 'fat_grams', 1000);
|
|
$consumedRaw = $body['consumed_at'] ?? null;
|
|
$consumed = is_string($consumedRaw) ? meal_api_date($consumedRaw) : null;
|
|
if ($consumed === null) api_json(['error' => 'invalid_consumed_at'], 400);
|
|
$consumedSql = $consumed->format('Y-m-d H:i:s');
|
|
$notes = $body['notes'] ?? '';
|
|
if (!is_string($notes) || mb_strlen($notes) > 1000) api_json(['error' => 'invalid_notes'], 400);
|
|
$name = trim($name);
|
|
$notes = trim($notes);
|
|
$userId = $auth['user_id'];
|
|
$connection = $db->getConnection();
|
|
$connection->begin_transaction();
|
|
$lookup = $db->prepare('SELECT id FROM meal_entries WHERE user_id = ? AND client_meal_uuid = ? FOR UPDATE');
|
|
$lookup->bind_param('is', $userId, $clientUuid);
|
|
$lookup->execute();
|
|
$existing = $lookup->get_result()->fetch_assoc();
|
|
$lookup->close();
|
|
if ($existing) {
|
|
$update = $db->prepare('UPDATE meal_entries SET name = ?, meal_type = ?, calories = ?, protein_grams = ?, carbohydrate_grams = ?, fat_grams = ?, consumed_at = ?, source = \'manual\', notes = ?, revision = revision + 1 WHERE user_id = ? AND client_meal_uuid = ?');
|
|
$update->bind_param('ssddddssis', $name, $mealType, $calories, $protein, $carbs, $fat, $consumedSql, $notes, $userId, $clientUuid);
|
|
$update->execute();
|
|
$update->close();
|
|
$status = 200;
|
|
} else {
|
|
$insert = $db->prepare('INSERT INTO meal_entries (user_id, client_meal_uuid, name, meal_type, calories, protein_grams, carbohydrate_grams, fat_grams, consumed_at, source, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, \'manual\', ?)');
|
|
$insert->bind_param('isssddddss', $userId, $clientUuid, $name, $mealType, $calories, $protein, $carbs, $fat, $consumedSql, $notes);
|
|
$insert->execute();
|
|
$insert->close();
|
|
$status = 201;
|
|
}
|
|
$connection->commit();
|
|
api_json(['status' => 'saved', 'client_meal_uuid' => $clientUuid], $status);
|
|
} catch (Throwable $exception) {
|
|
if (isset($connection) && $connection instanceof mysqli) {
|
|
try { $connection->rollback(); } catch (Throwable $ignored) {}
|
|
}
|
|
error_log('Meal journal request failed: ' . $exception->getMessage());
|
|
api_json(['error' => 'service_unavailable'], 503);
|
|
}
|