Files
drivetimeplaner/backup/old_api_20260520/app/Services/CalendarService.php
Hermes Agent 5d87e4975c Add HttpOnly cookie auth for cross-domain SPA login
Problem: Frontend JS bundle used credentials:'same-origin' which
dropped cookies on cross-domain requests. Login worked (200) but
the subsequent /auth/me check returned 401, leaving the user stuck
on the login screen.

Fix:
- AuthController now sets a dtp_jwt HttpOnly cookie on login/refresh
- Cookie uses Domain=.fahrschultermin.de (shared between frontend
  and api subdomain), Secure, SameSite=Lax, Max-Age=8h
- JwtMiddleware reads JWT from Authorization header OR cookie
- Added AuthController::me() endpoint (was missing, caused 500)
- Logout endpoint clears the cookie
- Frontend index.php patches fetch() to use credentials:'include'
  for all /api/v1/* calls
2026-06-04 20:33:31 +02:00

94 lines
3.3 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Services;
use App\Repositories\AppointmentRepository;
use App\Repositories\ReferenceDataRepository;
use DateInterval;
use DateTimeImmutable;
final class CalendarService
{
public function validate(int $tenantId, array $payload, ?int $appointmentId = null): array
{
$appointmentRepository = new AppointmentRepository();
$lessonType = (new ReferenceDataRepository())->findLessonType($tenantId, (int) $payload['lesson_type_id']);
$warnings = [
'conflicts' => [],
'rest_period_warnings' => [],
];
$conflicts = $appointmentRepository->overlappingForInstructor(
$tenantId,
(int) $payload['instructor_id'],
$payload['start_at'],
$payload['end_at'],
$appointmentId
);
foreach ($conflicts as $conflict) {
$warnings['conflicts'][] = [
'type' => 'instructor_overlap',
'message' => 'Fahrlehrer ist in diesem Zeitraum bereits verplant.',
'appointment_id' => (int) $conflict['id'],
];
}
if (!empty($payload['student_id'])) {
$studentConflicts = $appointmentRepository->overlappingForStudent(
$tenantId,
(int) $payload['student_id'],
$payload['start_at'],
$payload['end_at'],
$appointmentId
);
foreach ($studentConflicts as $conflict) {
$warnings['conflicts'][] = [
'type' => 'student_overlap',
'message' => 'Fahrschueler ist in diesem Zeitraum bereits verplant.',
'appointment_id' => (int) $conflict['id'],
];
}
}
if ($lessonType !== null && (int) $lessonType['is_rest_relevant'] === 1) {
$newStart = new DateTimeImmutable($payload['start_at']);
$dayStart = $newStart->setTime(0, 0, 0);
$dayEnd = $dayStart->modify('+1 day');
$hasLaterRelevantOnDay = $appointmentRepository->hasRestRelevantAfterStartOnDay(
$tenantId,
(int) $payload['instructor_id'],
$dayStart->format(DATE_ATOM),
$dayEnd->format(DATE_ATOM),
$payload['start_at']
);
if ($hasLaterRelevantOnDay) {
return $warnings;
}
$lastRelevant = $appointmentRepository->lastRestRelevantForInstructor(
$tenantId,
(int) $payload['instructor_id'],
$payload['start_at'],
$appointmentId
);
if ($lastRelevant !== null) {
$restEnds = (new DateTimeImmutable($lastRelevant['end_at']))->add(new DateInterval('PT11H'));
if ($newStart < $restEnds) {
$warnings['rest_period_warnings'][] = [
'type' => 'rest_period',
'message' => 'Achtung: Dieser Termin liegt innerhalb der 11-Stunden-Ruhezeit.',
'rest_until' => $restEnds->format(DATE_ATOM),
'blocking_appointment_id' => (int) $lastRelevant['id'],
];
}
}
}
return $warnings;
}
}