Fix tenant event form package selector so it no longer renders empty-value options, handles loading/empty

states, and pulls data from the authenticated /api/v1/tenant/packages endpoint.
    (resources/js/admin/pages/EventFormPage.tsx, resources/js/admin/api.ts)
  - Harden tenant-admin auth flow: prevent PKCE state loss, scope out StrictMode double-processing, add SPA
    routes for /event-admin/login and /event-admin/logout, and tighten token/session clearing semantics (resources/js/admin/auth/{context,tokens}.tsx, resources/js/admin/pages/{AuthCallbackPage,LogoutPage}.tsx,
    resources/js/admin/router.tsx, routes/web.php)
This commit is contained in:
Codex Agent
2025-10-19 23:00:47 +02:00
parent a949c8d3af
commit 6290a3a448
95 changed files with 3708 additions and 394 deletions

View File

@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
@viteReactRefresh
@vite('resources/js/admin/main.tsx')
@vite(['resources/css/app.css', 'resources/js/admin/main.tsx'])
</head>
<body>
<div id="root"></div>

View File

@@ -1,14 +1,22 @@
@php
$scriptNonce = $cspNonce ?? request()->attributes->get('csp_script_nonce');
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
@if($scriptNonce)
<meta name="csp-nonce" content="{{ $scriptNonce }}">
@endif
{{-- Inline script to detect system dark mode preference and apply it immediately --}}
<script>
<script @if($scriptNonce) nonce="{{ $scriptNonce }}" @endif>
(function() {
const appearance = '{{ $appearance ?? "system" }}';
window.__CSP_NONCE = '{{ $scriptNonce }}';
if (appearance === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -20,17 +28,6 @@
})();
</script>
{{-- Inline style to set the HTML background color based on our theme in app.css --}}
<style>
html {
background-color: oklch(1 0 0);
}
html.dark {
background-color: oklch(0.145 0 0);
}
</style>
<title inertia>{{ config('app.name', 'Laravel') }}</title>
<link rel="icon" href="/favicon.ico" sizes="any">
@@ -41,7 +38,7 @@
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
@viteReactRefresh
@vite(['resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"])
@vite(['resources/css/app.css', 'resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"])
@inertiaHead
</head>
<body class="font-sans antialiased">

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ __('emails.abandoned_checkout.subject_' . $timing, ['package' => $package->name]) }}</title>
<title>{{ __('emails.abandoned_checkout.subject_' . $timing, ['package' => $packageName]) }}</title>
<style>
.cta-button {
background-color: #007bff;
@@ -26,7 +26,7 @@
<body>
<h1>{{ __('emails.abandoned_checkout.greeting', ['name' => $user->fullName]) }}</h1>
<p>{{ __('emails.abandoned_checkout.body_' . $timing, ['package' => $package->name]) }}</p>
<p>{{ __('emails.abandoned_checkout.body_' . $timing, ['package' => $packageName]) }}</p>
<a href="{{ $resumeUrl }}" class="cta-button">
{{ __('emails.abandoned_checkout.cta_button') }}
@@ -44,4 +44,4 @@
<p>{!! __('emails.abandoned_checkout.footer') !!}</p>
</body>
</html>
</html>

View File

@@ -1,10 +1,7 @@
@component('mail::message')
# Hallo {{ $name }},
# {{ __('emails.contact_confirmation.greeting', ['name' => $name]) }}
vielen Dank fuer Ihre Nachricht an das Fotospiel Team. Wir melden uns so schnell wie moeglich bei Ihnen.
{{ __('emails.contact_confirmation.body') }}
Falls Sie weitere Informationen hinzufuegen moechten, antworten Sie einfach auf diese E-Mail.
Viele Gruesse
Ihr Fotospiel Team
{{ __('emails.contact_confirmation.footer') }}
@endcomponent

View File

@@ -1,13 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ __('emails.purchase.subject', ['package' => $package->name]) }}</title>
<title>{{ __('emails.purchase.subject', ['package' => $packageName]) }}</title>
</head>
<body>
<h1>{{ __('emails.purchase.greeting', ['name' => $user->fullName]) }}</h1>
<p>{{ __('emails.purchase.package', ['package' => $package->name]) }}</p>
<p>{{ __('emails.purchase.package', ['package' => $packageName]) }}</p>
<p>{{ __('emails.purchase.price', ['price' => $purchase->price]) }}</p>
<p>{{ __('emails.purchase.activation') }}</p>
<p>{!! __('emails.purchase.footer') !!}</p>
</body>
</html>
</html>

View File

@@ -68,6 +68,44 @@
</div>
</div>
@php
$analytics = $token['analytics'] ?? [];
@endphp
@if (!empty($analytics))
<div class="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-800 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-100">
<div class="text-[11px] uppercase tracking-wide">{{ __('admin.events.analytics.success_total') }}</div>
<div class="mt-1 text-lg font-semibold">
{{ number_format($analytics['success_total'] ?? 0) }}
</div>
</div>
<div class="rounded-lg border border-rose-200 bg-rose-50 p-3 text-xs text-rose-800 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-100">
<div class="text-[11px] uppercase tracking-wide">{{ __('admin.events.analytics.failure_total') }}</div>
<div class="mt-1 text-lg font-semibold">
{{ number_format($analytics['failure_total'] ?? 0) }}
</div>
</div>
<div class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100">
<div class="text-[11px] uppercase tracking-wide">{{ __('admin.events.analytics.rate_limited_total') }}</div>
<div class="mt-1 text-lg font-semibold">
{{ number_format($analytics['rate_limited_total'] ?? 0) }}
</div>
</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700 dark:border-slate-700 dark:bg-slate-800/70 dark:text-slate-200">
<div class="text-[11px] uppercase tracking-wide">{{ __('admin.events.analytics.recent_24h') }}</div>
<div class="mt-1 text-lg font-semibold">
{{ number_format($analytics['recent_24h'] ?? 0) }}
</div>
@if (!empty($analytics['last_seen_at']))
<div class="mt-1 text-[11px] text-slate-500 dark:text-slate-400">
{{ __('admin.events.analytics.last_seen_at', ['date' => \Carbon\Carbon::parse($analytics['last_seen_at'])->isoFormat('LLL')]) }}
</div>
@endif
</div>
</div>
@endif
@if (!empty($token['layouts']))
<div class="mt-4 space-y-3">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">

View File

@@ -1,3 +1,7 @@
@php
$scriptNonce = $cspNonce ?? request()->attributes->get('csp_script_nonce');
@endphp
<!doctype html>
<html lang="de">
<head>
@@ -7,7 +11,13 @@
<meta name="description" content="Sammle Gastfotos für Events mit QR-Codes und unserer PWA-Plattform. Für Hochzeiten, Firmenevents und mehr. Kostenloser Einstieg.">
<link rel="icon" href="{{ asset('logo.svg') }}" type="image/svg+xml">
<meta name="csrf-token" content="{{ csrf_token() }}">
@if($scriptNonce)
<meta name="csp-nonce" content="{{ $scriptNonce }}">
@endif
@vite(['resources/css/app.css', 'resources/js/app.tsx'])
<script @if($scriptNonce) nonce="{{ $scriptNonce }}" @endif>
window.__CSP_NONCE = '{{ $scriptNonce }}';
</script>
@php
$currentLocale = app()->getLocale();
@@ -20,17 +30,6 @@
<link rel="alternate" hreflang="{{ $locale }}" href="{{ url("/$locale$path") }}">
@endforeach
<link rel="alternate" hreflang="x-default" href="{{ url('/de' . $path) }}">
<style>
@keyframes aurora {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.bg-aurora {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: aurora 15s ease infinite;
}
</style>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-50 text-gray-900">

View File

@@ -186,7 +186,7 @@
@endsection
@push('scripts')
<script>
<script @if(isset($cspNonce) || request()->attributes->get('csp_script_nonce')) nonce="{{ $cspNonce ?? request()->attributes->get('csp_script_nonce') }}" @endif>
document.addEventListener('DOMContentLoaded', function() {
const tabLinks = document.querySelectorAll('.tab-link');
tabLinks.forEach(link => {
@@ -201,4 +201,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
</script>
@endpush
@endpush

View File

@@ -6,7 +6,7 @@
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
@auth
@if(auth()->user()->email_verified_at)
<script>
<script @if(isset($cspNonce) || request()->attributes->get('csp_script_nonce')) nonce="{{ $cspNonce ?? request()->attributes->get('csp_script_nonce') }}" @endif>
window.location.href = '/event-admin';
</script>
<div class="text-center">