- Login-System (Mock für Tests, Google OAuth vorbereitet) - Reisen erstellen mit Emoji, Ziel und Datum - Ablauf (Timeline), Packliste, Ideen, Notizen, Dokumente - Team-System: Personen einladen per E-Mail + Einladungslinks - Berechtigungen: Nur lesen / Bearbeiten / Admin - E-Mail-Benachrichtigungen beim Einladen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
237 lines
11 KiB
PHP
237 lines
11 KiB
PHP
<?php
|
|
date_default_timezone_set('Europe/Berlin');
|
|
|
|
define('SITE_ICON', '📑');
|
|
define('SITE_TITLE', 'Lütte Planer');
|
|
define('MAIL_FROM', 'jason@illg.de');
|
|
define('MAIL_FROM_NAME', 'Lütte Planer');
|
|
define('DATA_DIR', __DIR__ . '/data/');
|
|
define('UPLOAD_DIR', DATA_DIR . 'uploads/');
|
|
define('UPLOAD_MAX_MB', 10);
|
|
define('UPLOAD_ACCEPT', '.pdf,.jpg,.jpeg,.png,.gif,.webp,.doc,.docx');
|
|
|
|
// Google OAuth — wird später befüllt
|
|
define('GOOGLE_CLIENT_ID', '');
|
|
define('GOOGLE_CLIENT_SECRET', '');
|
|
define('GOOGLE_REDIRECT_URI', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/Develop/PlanerGoogel/auth/callback.php');
|
|
|
|
require_once __DIR__ . '/db.php';
|
|
|
|
// ── Session ───────────────────────────────────────────────────────────────────
|
|
|
|
function startSession(): void {
|
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
|
}
|
|
|
|
function requireLogin(): void {
|
|
startSession();
|
|
if (empty($_SESSION['user_id'])) {
|
|
header('Location: /Develop/PlanerGoogel/login.php'); exit;
|
|
}
|
|
}
|
|
|
|
function currentUser(): ?array {
|
|
startSession();
|
|
if (empty($_SESSION['user_id'])) return null;
|
|
return dbOne('SELECT * FROM users WHERE id = ?', [$_SESSION['user_id']]);
|
|
}
|
|
|
|
function loginUser(array $user): void {
|
|
startSession();
|
|
$_SESSION['user_id'] = $user['id'];
|
|
}
|
|
|
|
// ── Reisen-Helfer ─────────────────────────────────────────────────────────────
|
|
|
|
function getTrip(string $id): ?array {
|
|
return dbOne('SELECT * FROM reisen WHERE id = ?', [$id]);
|
|
}
|
|
|
|
function getTripMembers(string $tripId): array {
|
|
return dbAll(
|
|
'SELECT u.*, rz.access FROM users u
|
|
JOIN reise_zugriff rz ON rz.user_id = u.id
|
|
WHERE rz.trip_id = ?
|
|
ORDER BY rz.access DESC, u.name ASC',
|
|
[$tripId]
|
|
);
|
|
}
|
|
|
|
function userCanSee(string $tripId, int $userId): bool {
|
|
return dbOne('SELECT 1 FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]) !== null;
|
|
}
|
|
|
|
function userCanEdit(string $tripId, int $userId): bool {
|
|
$row = dbOne('SELECT access FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]);
|
|
return $row && in_array($row['access'], ['edit', 'admin']);
|
|
}
|
|
|
|
function userIsAdmin(string $tripId, int $userId): bool {
|
|
$row = dbOne('SELECT access FROM reise_zugriff WHERE trip_id = ? AND user_id = ?', [$tripId, $userId]);
|
|
return $row && $row['access'] === 'admin';
|
|
}
|
|
|
|
function requireTripAccess(string $tripId, bool $editRequired = false): array {
|
|
requireLogin();
|
|
$user = currentUser();
|
|
$trip = getTrip($tripId);
|
|
if (!$trip) { http_response_code(404); die('Reise nicht gefunden.'); }
|
|
if (!userCanSee($tripId, $user['id'])) { http_response_code(403); die('Kein Zugriff.'); }
|
|
if ($editRequired && !userCanEdit($tripId, $user['id'])) { http_response_code(403); die('Keine Berechtigung zum Bearbeiten.'); }
|
|
return [$trip, $user];
|
|
}
|
|
|
|
function newUuid(): string {
|
|
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
|
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff),
|
|
mt_rand(0,0x0fff)|0x4000, mt_rand(0,0x3fff)|0x8000,
|
|
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
|
|
);
|
|
}
|
|
|
|
// ── Datum-Helfer ──────────────────────────────────────────────────────────────
|
|
|
|
function germanDate(string $date): string {
|
|
if (!$date) return '';
|
|
$ts = strtotime($date);
|
|
$monate = ['Januar','Februar','März','April','Mai','Juni',
|
|
'Juli','August','September','Oktober','November','Dezember'];
|
|
return date('j', $ts) . '. ' . $monate[date('n', $ts) - 1] . ' ' . date('Y', $ts);
|
|
}
|
|
|
|
function daysUntil(string $date): int {
|
|
return (int) ceil((strtotime($date) - time()) / 86400);
|
|
}
|
|
|
|
// ── Avatar-Helfer ─────────────────────────────────────────────────────────────
|
|
|
|
function userInitials(string $name): string {
|
|
$parts = explode(' ', trim($name));
|
|
if (count($parts) >= 2) return strtoupper(mb_substr($parts[0], 0, 1) . mb_substr($parts[1], 0, 1));
|
|
return strtoupper(mb_substr($name, 0, 2));
|
|
}
|
|
|
|
function avatarHtml(array $user, string $size = '32px'): string {
|
|
$initials = userInitials($user['name']);
|
|
$farbe = htmlspecialchars($user['farbe'] ?? '#2563eb');
|
|
$name = htmlspecialchars($user['name']);
|
|
if (!empty($user['avatar_url'])) {
|
|
return "<img src=\"{$user['avatar_url']}\" alt=\"{$name}\" style=\"width:{$size};height:{$size};border-radius:50%;object-fit:cover;\" title=\"{$name}\">";
|
|
}
|
|
return "<span style=\"display:inline-flex;align-items:center;justify-content:center;width:{$size};height:{$size};border-radius:50%;background:{$farbe};color:#fff;font-size:calc({$size} * 0.38);font-weight:600;\" title=\"{$name}\">{$initials}</span>";
|
|
}
|
|
|
|
// ── XSS-Schutz ────────────────────────────────────────────────────────────────
|
|
|
|
function h(string $s): string {
|
|
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
// ── Packliste-Kategorien ──────────────────────────────────────────────────────
|
|
$PACKEN_KATEGORIEN = [
|
|
'kleidung' => ['label' => 'Kleidung', 'icon' => '👕'],
|
|
'dokumente' => ['label' => 'Dokumente', 'icon' => '📄'],
|
|
'elektronik' => ['label' => 'Elektronik', 'icon' => '🔌'],
|
|
'medizin' => ['label' => 'Medizin', 'icon' => '💊'],
|
|
'toilette' => ['label' => 'Toilette', 'icon' => '🧴'],
|
|
'essen' => ['label' => 'Essen & Trinken', 'icon' => '🍿'],
|
|
'strand' => ['label' => 'Strand', 'icon' => '🏖'],
|
|
'sport' => ['label' => 'Sport', 'icon' => '⚽'],
|
|
'sonstiges' => ['label' => 'Sonstiges', 'icon' => '📦'],
|
|
];
|
|
|
|
$UPLOAD_TYPEN = [
|
|
'application/pdf' => 'PDF',
|
|
'image/jpeg' => 'JPG',
|
|
'image/png' => 'PNG',
|
|
'image/gif' => 'GIF',
|
|
'image/webp' => 'WEBP',
|
|
'application/msword' => 'Word',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'Word',
|
|
];
|
|
|
|
// ── E-Mail-Versand ────────────────────────────────────────────────────────────
|
|
|
|
function sendMail(string $to, string $subject, string $htmlBody): bool {
|
|
$fromName = '=?UTF-8?B?' . base64_encode(MAIL_FROM_NAME) . '?=';
|
|
$subjectEncoded = '=?UTF-8?B?' . base64_encode($subject) . '?=';
|
|
$boundary = md5(uniqid());
|
|
|
|
$headers = "From: {$fromName} <" . MAIL_FROM . ">\r\n";
|
|
$headers .= "Reply-To: " . MAIL_FROM . "\r\n";
|
|
$headers .= "MIME-Version: 1.0\r\n";
|
|
$headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";
|
|
$headers .= "X-Mailer: PHP/" . phpversion();
|
|
|
|
$plain = strip_tags(str_replace(['<br>', '<br/>', '<br />', '</p>', '</div>'], "\n", $htmlBody));
|
|
$plain = preg_replace('/\n{3,}/', "\n\n", trim($plain));
|
|
|
|
$body = "--{$boundary}\r\n";
|
|
$body .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
$body .= "Content-Transfer-Encoding: base64\r\n\r\n";
|
|
$body .= chunk_split(base64_encode($plain)) . "\r\n";
|
|
$body .= "--{$boundary}\r\n";
|
|
$body .= "Content-Type: text/html; charset=UTF-8\r\n";
|
|
$body .= "Content-Transfer-Encoding: base64\r\n\r\n";
|
|
$body .= chunk_split(base64_encode($htmlBody)) . "\r\n";
|
|
$body .= "--{$boundary}--";
|
|
|
|
return mail($to, $subjectEncoded, $body, $headers);
|
|
}
|
|
|
|
function sendInviteMail(string $toEmail, string $tripName, string $inviteLink, string $inviterName): bool {
|
|
$subject = $inviterName . ' laedt dich zu "' . $tripName . '" ein';
|
|
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Inter,sans-serif;background:#f0f6ff;padding:30px;">
|
|
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;box-shadow:0 4px 20px rgba(37,99,235,.1);">
|
|
<div style="text-align:center;margin-bottom:24px;">
|
|
<div style="font-size:2.5rem;">📑</div>
|
|
<h1 style="font-family:Georgia,serif;font-size:1.4rem;color:#1d4ed8;margin:8px 0 4px;">Lütte Planer</h1>
|
|
</div>
|
|
<p style="color:#1e293b;font-size:.95rem;margin-bottom:12px;">Hallo!</p>
|
|
<p style="color:#1e293b;font-size:.95rem;margin-bottom:20px;">
|
|
<strong>' . htmlspecialchars($inviterName) . '</strong> hat dich zur Reise
|
|
<strong>„' . htmlspecialchars($tripName) . '"</strong> eingeladen.
|
|
</p>
|
|
<div style="text-align:center;margin:28px 0;">
|
|
<a href="' . htmlspecialchars($inviteLink) . '" style="background:#2563eb;color:#fff;padding:13px 28px;border-radius:8px;text-decoration:none;font-weight:600;font-size:.95rem;">
|
|
Einladung annehmen
|
|
</a>
|
|
</div>
|
|
<p style="color:#64748b;font-size:.78rem;margin-top:24px;">
|
|
Oder kopiere diesen Link in deinen Browser:<br>
|
|
<span style="color:#2563eb;word-break:break-all;">' . htmlspecialchars($inviteLink) . '</span>
|
|
</p>
|
|
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
|
|
<p style="color:#94a3b8;font-size:.72rem;text-align:center;">Lütte Planer · Reisen planen & teilen</p>
|
|
</div>
|
|
</body></html>';
|
|
|
|
return sendMail($toEmail, $subject, $html);
|
|
}
|
|
|
|
function sendAddedToTripMail(string $toEmail, string $toName, string $tripName, string $tripUrl, string $inviterName): bool {
|
|
$subject = 'Du wurdest zu "' . $tripName . '" hinzugefuegt';
|
|
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Inter,sans-serif;background:#f0f6ff;padding:30px;">
|
|
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;box-shadow:0 4px 20px rgba(37,99,235,.1);">
|
|
<div style="text-align:center;margin-bottom:24px;">
|
|
<div style="font-size:2.5rem;">📑</div>
|
|
<h1 style="font-family:Georgia,serif;font-size:1.4rem;color:#1d4ed8;margin:8px 0 4px;">Lütte Planer</h1>
|
|
</div>
|
|
<p style="color:#1e293b;font-size:.95rem;margin-bottom:12px;">Hallo ' . htmlspecialchars($toName) . '!</p>
|
|
<p style="color:#1e293b;font-size:.95rem;margin-bottom:20px;">
|
|
<strong>' . htmlspecialchars($inviterName) . '</strong> hat dich zur Reise
|
|
<strong>„' . htmlspecialchars($tripName) . '"</strong> hinzugefügt.
|
|
</p>
|
|
<div style="text-align:center;margin:28px 0;">
|
|
<a href="' . htmlspecialchars($tripUrl) . '" style="background:#2563eb;color:#fff;padding:13px 28px;border-radius:8px;text-decoration:none;font-weight:600;font-size:.95rem;">
|
|
Zur Reise
|
|
</a>
|
|
</div>
|
|
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
|
|
<p style="color:#94a3b8;font-size:.72rem;text-align:center;">Lütte Planer · Reisen planen & teilen</p>
|
|
</div>
|
|
</body></html>';
|
|
|
|
return sendMail($toEmail, $subject, $html);
|
|
}
|