Initial Release: Lütte Planer mit Google-Login-Vorbereitung
- 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>
This commit is contained in:
+236
@@ -0,0 +1,236 @@
|
||||
<?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);
|
||||
}
|
||||
Reference in New Issue
Block a user