upload, kleine thumbs
This commit is contained in:
@@ -0,0 +1,685 @@
|
||||
<?php
|
||||
/**
|
||||
* Single-file PHP API for multi-user "Igel" management
|
||||
* - Auth (JWT + Refresh)
|
||||
* - Igel CRUD
|
||||
* - Bilder (Liste/Upload/Löschen) – nutzt UPLOAD_* aus hedgehogs-settings.php
|
||||
* - Messwerte pro Igel (Liste/Anlegen/Aktualisieren/Löschen)
|
||||
*
|
||||
* Konfiguration: require_once 'hedgehogs-settings.php';
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// --- Load settings -----------------------------------------------------------
|
||||
require_once __DIR__ . '/hedgehogs-settings.php';
|
||||
|
||||
// --- PHP 7 polyfills ---------------------------------------------------------
|
||||
if (!function_exists('str_starts_with')) {
|
||||
function str_starts_with($haystack, $needle) { return $needle === '' || strpos($haystack, $needle) === 0; }
|
||||
}
|
||||
if (!function_exists('str_ends_with')) {
|
||||
function str_ends_with($haystack, $needle) {
|
||||
if ($needle === '') return true;
|
||||
$len = strlen($needle);
|
||||
return $len <= strlen($haystack) && substr($haystack, -$len) === $needle;
|
||||
}
|
||||
}
|
||||
|
||||
// --- CORS --------------------------------------------------------------------
|
||||
header('Vary: Origin');
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if ($origin && origin_allowed($origin, WH_ALLOWED_ORIGINS)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') { http_response_code(204); exit; }
|
||||
|
||||
// --- DB ----------------------------------------------------------------------
|
||||
$pdo = db();
|
||||
|
||||
// --- Router ------------------------------------------------------------------
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
|
||||
// Optional: alternative Routen-Param ?r=/path
|
||||
if (isset($_GET['r']) && is_string($_GET['r']) && $_GET['r'] !== '') {
|
||||
$path = $_GET['r'];
|
||||
}
|
||||
|
||||
// strip leading script name if fronted by /hedgehogs.php/...
|
||||
$script = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
if ($script && str_starts_with($path, $script)) {
|
||||
$path = substr($path, strlen($script));
|
||||
if ($path === '') $path = '/';
|
||||
}
|
||||
|
||||
try {
|
||||
// --- Auth ------------------------------------------------------------------
|
||||
if ($path === '/auth/register' && $method === 'POST') { return auth_register($pdo); }
|
||||
if ($path === '/auth/login' && $method === 'POST') { return auth_login($pdo); }
|
||||
if ($path === '/auth/refresh' && $method === 'POST') { return auth_refresh($pdo); }
|
||||
if ($path === '/auth/logout' && $method === 'POST') { return auth_logout($pdo); }
|
||||
|
||||
// --- Messwerte Update/Delete (TOP-LEVEL!) ---------------------------------
|
||||
// /messwerte/{id} → PUT/DELETE
|
||||
if (preg_match('#^/messwerte/(\d+)$#', $path, $m)) {
|
||||
$uid = require_user($pdo);
|
||||
$mid = (int)$m[1];
|
||||
if ($method === 'PUT') { return messwerte_update($pdo, $uid, $mid); }
|
||||
if ($method === 'DELETE') { return messwerte_delete($pdo, $uid, $mid); }
|
||||
}
|
||||
|
||||
// --- Igel + Unterressourcen -----------------------------------------------
|
||||
if (str_starts_with($path, '/igel')) {
|
||||
$uid = require_user($pdo);
|
||||
|
||||
// /igel
|
||||
if ($path === '/igel' && $method === 'GET') { return igel_list($pdo, $uid); }
|
||||
if ($path === '/igel' && $method === 'POST') { return igel_create($pdo, $uid); }
|
||||
|
||||
// /igel/{id}
|
||||
if (preg_match('#^/igel/(\d+)$#', $path, $m)) {
|
||||
$id = (int)$m[1];
|
||||
if ($method === 'GET') { return igel_get($pdo, $uid, $id); }
|
||||
if ($method === 'PUT') { return igel_update($pdo, $uid, $id); }
|
||||
if ($method === 'DELETE') { return igel_delete($pdo, $uid, $id); }
|
||||
}
|
||||
|
||||
// /igel/{id}/images
|
||||
if (preg_match('#^/igel/(\d+)/images$#', $path, $m)) {
|
||||
$igId = (int)$m[1];
|
||||
if ($method === 'GET') { return igel_images_list($pdo, $uid, $igId); }
|
||||
if ($method === 'POST') { return igel_images_upload($pdo, $uid, $igId); }
|
||||
}
|
||||
|
||||
// /images/{imgId}
|
||||
if (preg_match('#^/images/(\d+)$#', $path, $m)) {
|
||||
$imgId = (int)$m[1];
|
||||
if ($method === 'DELETE') { return igel_images_delete($pdo, $uid, $imgId); }
|
||||
}
|
||||
|
||||
// /igel/{id}/messwerte (Liste + Neu)
|
||||
if (preg_match('#^/igel/(\d+)/messwerte$#', $path, $m)) {
|
||||
$igId = (int)$m[1];
|
||||
if ($method === 'GET') { return messwerte_list($pdo, $uid, $igId); }
|
||||
if ($method === 'POST') { return messwerte_create($pdo, $uid, $igId); }
|
||||
}
|
||||
}
|
||||
|
||||
json(['error' => 'Not Found', 'path' => $path], 404);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[hedgehogs.php] Exception: '.$e->getMessage());
|
||||
json(['error' => 'Server error'], 500);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUTH
|
||||
// =============================================================================
|
||||
|
||||
function auth_register(PDO $pdo): void {
|
||||
$in = body_json();
|
||||
$email = strtolower(trim((string)($in['email'] ?? '')));
|
||||
$pass = (string)($in['password'] ?? '');
|
||||
|
||||
// E-Mail-Check ohne filter-Extension
|
||||
$emailOk = (bool)preg_match('/^[^\s@]+@[^\s@]+\.[^\s@]+$/', $email);
|
||||
if (!$emailOk || strlen($pass) < 8) { json(['error' => 'Invalid input'], 422); return; }
|
||||
|
||||
$hash = password_hash($pass, defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_DEFAULT);
|
||||
try {
|
||||
$stmt = $pdo->prepare('INSERT INTO users(email, password_hash) VALUES(?, ?)');
|
||||
$stmt->execute([$email, $hash]);
|
||||
} catch (PDOException $e) {
|
||||
if ((int)($e->errorInfo[1] ?? 0) === 1062) { json(['error' => 'Email already exists'], 409); return; }
|
||||
throw $e;
|
||||
}
|
||||
json(['ok' => true], 201);
|
||||
}
|
||||
|
||||
function auth_login(PDO $pdo): void {
|
||||
$in = body_json();
|
||||
$email = strtolower(trim((string)($in['email'] ?? '')));
|
||||
$pass = (string)($in['password'] ?? '');
|
||||
|
||||
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = ?');
|
||||
$stmt->execute([$email]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row || !password_verify($pass, (string)$row['password_hash'])) {
|
||||
json(['error' => 'Invalid credentials'], 401); return;
|
||||
}
|
||||
$uid = (int)$row['id'];
|
||||
[$access, $refresh] = issue_tokens($pdo, $uid);
|
||||
json(['access_token' => $access, 'refresh_token' => $refresh]);
|
||||
}
|
||||
|
||||
function auth_refresh(PDO $pdo): void {
|
||||
$in = body_json();
|
||||
$refresh = (string)($in['refresh_token'] ?? '');
|
||||
$stmt = $pdo->prepare('SELECT user_id FROM refresh_tokens WHERE token = ? AND expires_at > NOW()');
|
||||
$stmt->execute([$refresh]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) { json(['error' => 'Invalid refresh'], 401); return; }
|
||||
$uid = (int)$row['user_id'];
|
||||
$now = time();
|
||||
$access = jwt_encode(['iss' => JWT_ISSUER, 'iat' => $now, 'exp' => $now + JWT_ACCESS_TTL, 'sub' => $uid], JWT_SECRET);
|
||||
json(['access_token' => $access]);
|
||||
}
|
||||
|
||||
function auth_logout(PDO $pdo): void {
|
||||
$in = body_json();
|
||||
$refresh = (string)($in['refresh_token'] ?? '');
|
||||
$stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE token = ?');
|
||||
$stmt->execute([$refresh]);
|
||||
json(['ok' => true]);
|
||||
}
|
||||
|
||||
function require_user(PDO $pdo): int {
|
||||
$hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
if (!preg_match('/Bearer\s+(.*)/i', $hdr, $m)) { json(['error' => 'Unauthorized'], 401); exit; }
|
||||
try {
|
||||
$payload = jwt_decode($m[1], JWT_SECRET);
|
||||
if (($payload['iss'] ?? null) !== JWT_ISSUER) throw new Exception('bad iss');
|
||||
$sub = (int)($payload['sub'] ?? 0);
|
||||
if ($sub <= 0) throw new Exception('bad sub');
|
||||
return $sub;
|
||||
} catch (Throwable $e) {
|
||||
json(['error' => 'Unauthorized'], 401); exit;
|
||||
}
|
||||
}
|
||||
|
||||
function issue_tokens(PDO $pdo, int $uid): array {
|
||||
$now = time();
|
||||
$access = jwt_encode(['iss' => JWT_ISSUER, 'iat' => $now, 'exp' => $now + JWT_ACCESS_TTL, 'sub' => $uid], JWT_SECRET);
|
||||
$refresh = bin2hex(random_bytes(32));
|
||||
$stmt = $pdo->prepare('INSERT INTO refresh_tokens(user_id, token, expires_at) VALUES(?,?, FROM_UNIXTIME(?))');
|
||||
$stmt->execute([$uid, $refresh, $now + JWT_REFRESH_TTL]);
|
||||
return [$access, $refresh];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// IGEL
|
||||
// =============================================================================
|
||||
|
||||
function igel_list(PDO $pdo, int $uid): void {
|
||||
$stmt = $pdo->prepare('SELECT id, name, gender, feature, note, created_at, updated_at FROM igel WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$uid]);
|
||||
json($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
function igel_create(PDO $pdo, int $uid): void {
|
||||
$in = body_json();
|
||||
$name = trim((string)($in['name'] ?? ''));
|
||||
$gender = isset($in['gender']) ? (string)$in['gender'] : null;
|
||||
$feature = isset($in['feature']) ? (string)$in['feature'] : null;
|
||||
$note = isset($in['note']) ? (string)$in['note'] : null;
|
||||
|
||||
if ($name === '') { json(['error' => 'Name required'], 422); return; }
|
||||
$stmt = $pdo->prepare('INSERT INTO igel(user_id, name, gender, feature, note) VALUES(?,?,?,?,?)');
|
||||
$stmt->execute([$uid, $name, $gender, $feature, $note]);
|
||||
$id = (int)$pdo->lastInsertId();
|
||||
json(['id' => $id, 'name' => $name, 'gender' => $gender, 'feature' => $feature, 'note' => $note], 201);
|
||||
}
|
||||
|
||||
function igel_get(PDO $pdo, int $uid, int $id): void {
|
||||
$stmt = $pdo->prepare('SELECT id, name, gender, feature, note, created_at, updated_at FROM igel WHERE id=? AND user_id=?');
|
||||
$stmt->execute([$id, $uid]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) { json(['error' => 'Not found'], 404); return; }
|
||||
json($row);
|
||||
}
|
||||
|
||||
function igel_update(PDO $pdo, int $uid, int $id): void {
|
||||
$in = body_json();
|
||||
$name = trim((string)($in['name'] ?? ''));
|
||||
$gender = isset($in['gender']) ? (string)$in['gender'] : null;
|
||||
$feature = isset($in['feature']) ? (string)$in['feature'] : null;
|
||||
$note = isset($in['note']) ? (string)$in['note'] : null;
|
||||
|
||||
if ($name === '') { json(['error' => 'Name required'], 422); return; }
|
||||
$stmt = $pdo->prepare('UPDATE igel SET name=?, gender=?, feature=?, note=? WHERE id=? AND user_id=?');
|
||||
$stmt->execute([$name, $gender, $feature, $note, $id, $uid]);
|
||||
json(['ok' => true]);
|
||||
}
|
||||
|
||||
function igel_delete(PDO $pdo, int $uid, int $id): void {
|
||||
$stmt = $pdo->prepare('DELETE FROM igel WHERE id=? AND user_id=?');
|
||||
$stmt->execute([$id, $uid]);
|
||||
json(['ok' => true]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// IGEL BILDER (Liste/Upload/Löschen)
|
||||
// =============================================================================
|
||||
|
||||
function igel_images_list(PDO $pdo, int $uid, int $igId): void {
|
||||
// Besitz prüfen
|
||||
$own=$pdo->prepare('SELECT id FROM igel WHERE id=? AND user_id=?');
|
||||
$own->execute([$igId,$uid]);
|
||||
if(!$own->fetch()) { json(['error'=>'Not found'],404); return; }
|
||||
|
||||
try {
|
||||
$stmt=$pdo->prepare('SELECT id,url,thumb_url,original_name,mime,size_bytes,created_at,taken_at
|
||||
FROM igel_images WHERE igel_id=? ORDER BY id DESC');
|
||||
$stmt->execute([$igId]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
// Fallback, falls thumb_url-Spalte fehlt
|
||||
$stmt=$pdo->prepare('SELECT id,url,original_name,mime,size_bytes,created_at,taken_at
|
||||
FROM igel_images WHERE igel_id=? ORDER BY id DESC');
|
||||
$stmt->execute([$igId]);
|
||||
$tmp = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$rows = [];
|
||||
foreach ($tmp as $r) {
|
||||
$r['thumb_url'] = $r['url'];
|
||||
$rows[] = $r;
|
||||
}
|
||||
}
|
||||
json($rows);
|
||||
}
|
||||
|
||||
function igel_images_upload(PDO $pdo, int $uid, int $igId): void {
|
||||
// Besitz prüfen
|
||||
$own=$pdo->prepare('SELECT id FROM igel WHERE id=? AND user_id=?');
|
||||
$own->execute([$igId,$uid]);
|
||||
if(!$own->fetch()) { json(['error'=>'Not found'],404); return; }
|
||||
|
||||
// multipart/form-data: files[]
|
||||
if (!isset($_FILES['files'])) { json(['error' => 'No files'], 400); return; }
|
||||
$files = $_FILES['files'];
|
||||
|
||||
// Optionales paralleles Feld: taken_at[] (ISO-8601 vom Client aus EXIF)
|
||||
$takenArr = [];
|
||||
if (isset($_POST['taken_at'])) {
|
||||
$takenArr = is_array($_POST['taken_at']) ? $_POST['taken_at'] : [$_POST['taken_at']];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$count = is_array($files['name']) ? count($files['name']) : 0;
|
||||
for ($i=0; $i<$count; $i++) {
|
||||
if ((int)$files['error'][$i] !== UPLOAD_ERR_OK) continue;
|
||||
$tmp = (string)$files['tmp_name'][$i];
|
||||
$orig = (string)$files['name'][$i];
|
||||
$size = (int)$files['size'][$i];
|
||||
|
||||
if ($size <= 0 || $size > MAX_IMAGE_SIZE) continue;
|
||||
|
||||
// MIME grob anhand Endung
|
||||
$lower = strtolower($orig);
|
||||
$ext = '.bin';
|
||||
$mime = 'application/octet-stream';
|
||||
if (preg_match('/\.(jpg|jpeg)$/', $lower)) { $ext = '.jpg'; $mime='image/jpeg'; }
|
||||
elseif (preg_match('/\.png$/', $lower)) { $ext = '.png'; $mime='image/png'; }
|
||||
elseif (preg_match('/\.webp$/', $lower)) { $ext = '.webp'; $mime='image/webp'; }
|
||||
elseif (preg_match('/\.gif$/', $lower)) { $ext = '.gif'; $mime='image/gif'; }
|
||||
|
||||
// sichere Dateinamen
|
||||
$base = bin2hex(random_bytes(8));
|
||||
$fn = $base . $ext;
|
||||
$destDir = rtrim(UPLOAD_DIR, '/');
|
||||
if (!is_dir($destDir)) { @mkdir($destDir, 0755, true); }
|
||||
$dest = $destDir . '/' . $fn;
|
||||
|
||||
if (!move_uploaded_file($tmp, $dest)) continue;
|
||||
|
||||
// Thumb
|
||||
$thumbUrl = null;
|
||||
try {
|
||||
$thumbDir = rtrim(UPLOAD_THUMB_DIR, '/');
|
||||
if (!is_dir($thumbDir)) { @mkdir($thumbDir, 0755, true); }
|
||||
$thumbPath = $thumbDir . '/' . $fn;
|
||||
create_thumbnail($dest, $thumbPath, 512, 512); // Quadrat-Box
|
||||
$thumbUrl = rtrim(UPLOAD_BASE_URL,'/') . '/thumbs/' . $fn;
|
||||
} catch (Throwable $e) {
|
||||
$thumbUrl = null; // ok
|
||||
}
|
||||
|
||||
$url = rtrim(UPLOAD_BASE_URL,'/') . '/' . $fn;
|
||||
|
||||
// EXIF-Aufnahmezeit (taken_at[]) → DATETIME oder NULL
|
||||
$takenAtMysql = null;
|
||||
if (isset($takenArr[$i])) {
|
||||
$raw = (string)$takenArr[$i];
|
||||
$ts = strtotime($raw);
|
||||
if ($ts !== false) {
|
||||
$takenAtMysql = date('Y-m-d H:i:s', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
// DB: created_at via DEFAULT CURRENT_TIMESTAMP, taken_at separat speichern
|
||||
$stmt = $pdo->prepare('INSERT INTO igel_images
|
||||
(igel_id, url, thumb_url, original_name, mime, size_bytes, taken_at)
|
||||
VALUES(?,?,?,?,?,?,?)');
|
||||
$stmt->execute([$igId, $url, $thumbUrl, $orig, $mime, $size, $takenAtMysql]);
|
||||
|
||||
$id = (int)$pdo->lastInsertId();
|
||||
|
||||
// created_at aus DB holen (für Response, damit Frontend sofort beides hat)
|
||||
$row = $pdo->prepare('SELECT created_at, taken_at FROM igel_images WHERE id=?');
|
||||
$row->execute([$id]);
|
||||
$times = $row->fetch(PDO::FETCH_ASSOC) ?: ['created_at'=>null,'taken_at'=>null];
|
||||
|
||||
$out[] = [
|
||||
'id' => $id,
|
||||
'url' => $url,
|
||||
'thumb_url' => $thumbUrl,
|
||||
'original_name' => $orig,
|
||||
'mime' => $mime,
|
||||
'size_bytes' => $size,
|
||||
'created_at' => (string)($times['created_at'] ?? ''),
|
||||
'taken_at' => (string)($times['taken_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
json($out, 201);
|
||||
}
|
||||
|
||||
function igel_images_delete(PDO $pdo, int $uid, int $imgId): void {
|
||||
// Besitz prüfen (Join)
|
||||
$stmt=$pdo->prepare('SELECT i.id, i.url, i.thumb_url
|
||||
FROM igel_images i
|
||||
JOIN igel g ON g.id = i.igel_id
|
||||
WHERE i.id=? AND g.user_id=?');
|
||||
$stmt->execute([$imgId,$uid]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) { json(['error'=>'Not found'],404); return; }
|
||||
|
||||
// Dateien optional entfernen
|
||||
try {
|
||||
$url = (string)$row['url'];
|
||||
$thumb = (string)($row['thumb_url'] ?? '');
|
||||
$fn = basename(parse_url($url, PHP_URL_PATH) ?? '');
|
||||
$fnT = $thumb ? basename(parse_url($thumb, PHP_URL_PATH) ?? '') : '';
|
||||
$p = rtrim(UPLOAD_DIR,'/').'/'.$fn;
|
||||
if (is_file($p)) @unlink($p);
|
||||
if ($fnT !== '') {
|
||||
$pt = rtrim(UPLOAD_THUMB_DIR,'/').'/'.$fnT;
|
||||
if (is_file($pt)) @unlink($pt);
|
||||
}
|
||||
} catch (Throwable $e) {}
|
||||
|
||||
$del=$pdo->prepare('DELETE FROM igel_images WHERE id=?');
|
||||
$del->execute([$imgId]);
|
||||
json(['ok'=>true]);
|
||||
}
|
||||
|
||||
// --- Thumbnail Helper (GD) ---------------------------------------------------
|
||||
|
||||
function create_thumbnail(string $src, string $dest, int $maxW, int $maxH): void {
|
||||
if (!extension_loaded('gd')) throw new Exception('GD not loaded');
|
||||
[$w,$h,$type] = getimagesize($src);
|
||||
if (!$w || !$h) throw new Exception('bad image');
|
||||
|
||||
switch ($type) {
|
||||
case IMAGETYPE_JPEG: $im = imagecreatefromjpeg($src); break;
|
||||
case IMAGETYPE_PNG: $im = imagecreatefrompng($src); break;
|
||||
case IMAGETYPE_WEBP: if (function_exists('imagecreatefromwebp')) { $im = imagecreatefromwebp($src); } else { throw new Exception('webp not supported'); } break;
|
||||
case IMAGETYPE_GIF: $im = imagecreatefromgif($src); break;
|
||||
default: throw new Exception('unsupported type');
|
||||
}
|
||||
|
||||
$ratio = min($maxW/$w, $maxH/$h, 1.0);
|
||||
$nw = (int)round($w*$ratio);
|
||||
$nh = (int)round($h*$ratio);
|
||||
$thumb = imagecreatetruecolor($nw, $nh);
|
||||
|
||||
// transparent für PNG/GIF
|
||||
if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_GIF], true)) {
|
||||
imagecolortransparent($thumb, imagecolorallocatealpha($thumb, 0, 0, 0, 127));
|
||||
imagealphablending($thumb, false);
|
||||
imagesavealpha($thumb, true);
|
||||
}
|
||||
|
||||
imagecopyresampled($thumb, $im, 0,0,0,0, $nw,$nh,$w,$h);
|
||||
|
||||
$ext = strtolower(pathinfo($dest, PATHINFO_EXTENSION));
|
||||
if ($ext === 'png') imagepng($thumb, $dest, 6);
|
||||
elseif ($ext === 'gif') imagegif($thumb, $dest);
|
||||
elseif ($ext === 'webp' && function_exists('imagewebp')) imagewebp($thumb, $dest, 85);
|
||||
else imagejpeg($thumb, $dest, 85);
|
||||
|
||||
imagedestroy($im);
|
||||
imagedestroy($thumb);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MESSWERTE
|
||||
// =============================================================================
|
||||
|
||||
function messwerte_list(PDO $pdo, int $uid, int $igId): void {
|
||||
// Besitz
|
||||
$own=$pdo->prepare('SELECT id FROM igel WHERE id=? AND user_id=?');
|
||||
$own->execute([$igId,$uid]);
|
||||
if(!$own->fetch()) { json(['error'=>'Not found'],404); return; }
|
||||
|
||||
$stmt=$pdo->prepare('SELECT id, igel_id, DATE_FORMAT(datum, "%Y-%m-%dT%H:%i:%sZ") AS datum,
|
||||
gewicht, behandlung, bemerkung, created_at
|
||||
FROM messwerte
|
||||
WHERE igel_id=? ORDER BY datum DESC, id DESC');
|
||||
$stmt->execute([$igId]);
|
||||
json($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
function messwerte_create(PDO $pdo, int $uid, int $igId): void {
|
||||
// Besitz
|
||||
$own=$pdo->prepare('SELECT id FROM igel WHERE id=? AND user_id=?');
|
||||
$own->execute([$igId,$uid]);
|
||||
if(!$own->fetch()) { json(['error'=>'Not found'],404); return; }
|
||||
|
||||
$in = body_json();
|
||||
$datumRaw = (string)($in['datum'] ?? '');
|
||||
$gewicht = (int)($in['gewicht'] ?? 0);
|
||||
$behandlung = isset($in['behandlung']) ? (string)$in['behandlung'] : null;
|
||||
$bemerkung = isset($in['bemerkung']) ? (string)$in['bemerkung'] : null;
|
||||
|
||||
// Datum akzeptiert ISO-8601 oder "YYYY-MM-DD HH:MM"
|
||||
$ts = $datumRaw !== '' ? strtotime($datumRaw) : time();
|
||||
if ($ts === false) { json(['error'=>'Invalid date'],422); return; }
|
||||
if ($gewicht < 1 || $gewicht > 100000) { json(['error'=>'Invalid weight'],422); return; }
|
||||
|
||||
$mysql = date('Y-m-d H:i:s', $ts);
|
||||
|
||||
$stmt=$pdo->prepare('INSERT INTO messwerte (igel_id, datum, gewicht, behandlung, bemerkung) VALUES(?,?,?,?,?)');
|
||||
$stmt->execute([$igId, $mysql, $gewicht, $behandlung, $bemerkung]);
|
||||
$id = (int)$pdo->lastInsertId();
|
||||
|
||||
json([
|
||||
'id'=>$id,
|
||||
'igel_id'=>$igId,
|
||||
'datum'=>gmdate('Y-m-d\TH:i:s\Z', $ts),
|
||||
'gewicht'=>$gewicht,
|
||||
'behandlung'=>$behandlung,
|
||||
'bemerkung'=>$bemerkung
|
||||
], 201);
|
||||
}
|
||||
|
||||
function messwerte_update(PDO $pdo, int $uid, int $mid): void {
|
||||
// Besitz via Join prüfen
|
||||
$own=$pdo->prepare('SELECT m.id, m.igel_id FROM messwerte m JOIN igel g ON g.id=m.igel_id WHERE m.id=? AND g.user_id=?');
|
||||
$own->execute([$mid,$uid]);
|
||||
$row=$own->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$row) { json(['error'=>'Not found'],404); return; }
|
||||
|
||||
$in = body_json();
|
||||
// Alle Felder optional, aber validieren, falls vorhanden
|
||||
$set = [];
|
||||
$args= [];
|
||||
|
||||
if (isset($in['datum'])) {
|
||||
$ts = strtotime((string)$in['datum']);
|
||||
if ($ts === false) { json(['error'=>'Invalid date'],422); return; }
|
||||
$set[]='datum=?'; $args[]=date('Y-m-d H:i:s',$ts);
|
||||
}
|
||||
if (isset($in['gewicht'])) {
|
||||
$gewicht=(int)$in['gewicht'];
|
||||
if ($gewicht < 1 || $gewicht > 100000) { json(['error'=>'Invalid weight'],422); return; }
|
||||
$set[]='gewicht=?'; $args[]=$gewicht;
|
||||
}
|
||||
if (array_key_exists('behandlung',$in)) { $set[]='behandlung=?'; $args[]=(string)$in['behandlung']; }
|
||||
if (array_key_exists('bemerkung',$in)) { $set[]='bemerkung=?'; $args[]=(string)$in['bemerkung']; }
|
||||
|
||||
if (empty($set)) { json(['error'=>'No fields'],400); return; }
|
||||
|
||||
$args[]=$mid;
|
||||
$sql='UPDATE messwerte SET '.implode(',', $set).' WHERE id=?';
|
||||
$stmt=$pdo->prepare($sql);
|
||||
$stmt->execute($args);
|
||||
json(['ok'=>true]);
|
||||
}
|
||||
|
||||
function messwerte_delete(PDO $pdo, int $uid, int $mid): void {
|
||||
// Besitz via Join prüfen
|
||||
$own=$pdo->prepare('SELECT m.id FROM messwerte m JOIN igel g ON g.id=m.igel_id WHERE m.id=? AND g.user_id=?');
|
||||
$own->execute([$mid,$uid]);
|
||||
if(!$own->fetch()) { json(['error'=>'Not found'],404); return; }
|
||||
|
||||
$del=$pdo->prepare('DELETE FROM messwerte WHERE id=?');
|
||||
$del->execute([$mid]);
|
||||
json(['ok'=>true]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utilities
|
||||
// =============================================================================
|
||||
|
||||
function json($data, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
function body_json(): array {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === false || $raw === '') return [];
|
||||
$data = json_decode($raw, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
function db(): PDO {
|
||||
$dsn = 'mysql:host=' . DATABASE_HOST . ';dbname=' . DATABASE_NAME . ';charset=utf8mb4';
|
||||
$pdo = new PDO($dsn, DATABASE_USER, DATABASE_PASSWORD, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function origin_allowed(string $origin, array $allowed): bool {
|
||||
$u = parse_url($origin);
|
||||
if (!$u || !isset($u['scheme'], $u['host'])) return false;
|
||||
$oScheme = $u['scheme']; $oHost = $u['host']; $oPort = (string)($u['port'] ?? '');
|
||||
foreach ($allowed as $pat) {
|
||||
$pu = parse_url($pat);
|
||||
if (!$pu || !isset($pu['scheme'])) continue;
|
||||
if ($pu['scheme'] !== $oScheme) continue;
|
||||
$pHost = $pu['host'] ?? '';
|
||||
$pPort = $pu['port'] ?? '';
|
||||
$hostOk = false;
|
||||
if ($pHost === $oHost) $hostOk = true;
|
||||
elseif (str_starts_with($pHost, '*.' )) { $suffix = substr($pHost, 1); if (str_ends_with($oHost, $suffix)) $hostOk = true; }
|
||||
elseif ($pHost === '' && isset($pu['path'])) {
|
||||
$p = $pu['path']; // z.B. localhost:*
|
||||
if ($p === $oHost || (str_starts_with($p, '*.') && str_ends_with($oHost, substr($p,1)))) $hostOk = true;
|
||||
}
|
||||
if (!$hostOk) continue;
|
||||
$patHasWildcardPort = str_ends_with($pat, ':*');
|
||||
$portOk = $patHasWildcardPort || ($pPort !== '' && (string)$pPort === $oPort) || ($pPort === '' && $oPort === '');
|
||||
if ($portOk) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Minimal JWT HS256 -------------------------------------------------------
|
||||
|
||||
function b64url_encode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }
|
||||
function b64url_decode(string $data): string { return base64_decode(strtr($data, '-_', '+/')) ?: ''; }
|
||||
|
||||
function jwt_encode(array $payload, string $secret): string {
|
||||
$header = ['typ' => 'JWT', 'alg' => 'HS256'];
|
||||
$segments = [b64url_encode(json_encode($header)), b64url_encode(json_encode($payload))];
|
||||
$signingInput = implode('.', $segments);
|
||||
$signature = hash_hmac('sha256', $signingInput, $secret, true);
|
||||
$segments[] = b64url_encode($signature);
|
||||
return implode('.', $segments);
|
||||
}
|
||||
|
||||
function jwt_decode(string $token, string $secret): array {
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) !== 3) throw new Exception('bad token');
|
||||
[$h64, $p64, $s64] = $parts;
|
||||
$header = json_decode(b64url_decode($h64), true) ?: [];
|
||||
if (($header['alg'] ?? '') !== 'HS256') throw new Exception('alg');
|
||||
$payload = json_decode(b64url_decode($p64), true) ?: [];
|
||||
$sig = b64url_decode($s64);
|
||||
$expected = hash_hmac('sha256', "$h64.$p64", $secret, true);
|
||||
if (!hash_equals($expected, $sig)) throw new Exception('sig');
|
||||
if (isset($payload['exp']) && time() >= (int)$payload['exp']) throw new Exception('exp');
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/*
|
||||
-- SQL Reference (run once)
|
||||
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
email VARCHAR(191) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE refresh_tokens (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
token VARCHAR(255) NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX (user_id), INDEX (token)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE igel (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
gender VARCHAR(30) NULL,
|
||||
feature VARCHAR(255) NULL,
|
||||
note TEXT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX (user_id), INDEX (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE igel_images (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
igel_id BIGINT UNSIGNED NOT NULL,
|
||||
url VARCHAR(500) NOT NULL,
|
||||
thumb_url VARCHAR(500) NULL,
|
||||
original_name VARCHAR(255) NULL,
|
||||
mime VARCHAR(100) NULL,
|
||||
size_bytes BIGINT UNSIGNED NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (igel_id) REFERENCES igel(id) ON DELETE CASCADE,
|
||||
INDEX (igel_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE messwerte (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
igel_id BIGINT UNSIGNED NOT NULL,
|
||||
datum DATETIME NOT NULL,
|
||||
gewicht INT UNSIGNED NOT NULL,
|
||||
behandlung VARCHAR(255) NULL,
|
||||
bemerkung TEXT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (igel_id) REFERENCES igel(id) ON DELETE CASCADE,
|
||||
INDEX (igel_id), INDEX (datum)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
*/
|
||||
?>
|
||||
Reference in New Issue
Block a user