From 52197f216df97af8d201f729e5ae5ea4300a1d06 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 10 Oct 2025 15:20:52 +0200 Subject: [PATCH] Route tenant admin PWA via /event-admin --- app/Http/Controllers/MarketingController.php | 10 +++-- app/Http/Controllers/OAuthController.php | 13 +++--- docs/prp/tenant-app-specs/api-usage.md | 3 +- docs/prp/tenant-app-specs/capacitor-setup.md | 4 +- resources/js/admin/auth/tokens.ts | 3 +- resources/js/admin/components/AdminLayout.tsx | 6 ++- resources/js/admin/constants.ts | 9 ++++ resources/js/admin/pages/AuthCallbackPage.tsx | 4 +- resources/js/admin/pages/EventDetailPage.tsx | 8 ++-- resources/js/admin/pages/EventFormPage.tsx | 45 ++++++++++--------- resources/js/admin/pages/EventPhotosPage.tsx | 6 ++- resources/js/admin/pages/EventsPage.tsx | 14 +++--- resources/js/admin/pages/LoginPage.tsx | 10 +++-- resources/js/admin/pages/SettingsPage.tsx | 6 ++- resources/js/admin/router.tsx | 15 ++++--- resources/js/pages/marketing/Success.tsx | 7 +-- resources/views/marketing/success.blade.php | 4 +- routes/web.php | 4 +- tests/Feature/FullUserFlowTest.php | 4 +- tests/e2e/package-flow.test.ts | 12 ++--- 20 files changed, 112 insertions(+), 75 deletions(-) create mode 100644 resources/js/admin/constants.ts diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index 0586585..56d8cfa 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Mail\ContactConfirmation; use Illuminate\Http\Request; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Log; @@ -53,6 +54,8 @@ class MarketingController extends Controller ->subject('Neue Kontakt-Anfrage'); }); + Mail::to($request->email)->queue(new ContactConfirmation($request->name)); + return redirect()->back()->with('success', 'Nachricht gesendet!'); } @@ -109,7 +112,7 @@ class MarketingController extends Controller 'refunded' => false, ]); - return redirect('/admin')->with('success', __('marketing.packages.free_assigned')); + return redirect('/event-admin')->with('success', __('marketing.packages.free_assigned')); } if ($package->type === 'reseller') { @@ -338,13 +341,13 @@ class MarketingController extends Controller $request->session()->flash('error', 'Zahlung konnte nicht abgeschlossen werden.'); } catch (\Exception $e) { Log::error('PayPal success error: ' . $e->getMessage()); - $request->session()->flash('error', 'Fehler beim Abschließen der Zahlung.'); + $request->session()->flash('error', 'Fehler beim Abschliessen der Zahlung.'); } } // Common logic: Redirect to admin if verified if (Auth::check() && Auth::user()->email_verified_at) { - return redirect('/admin')->with('success', __('marketing.success.welcome')); + return redirect('/event-admin')->with('success', __('marketing.success.welcome')); } return Inertia::render('marketing/Success', compact('packageId')); @@ -434,3 +437,4 @@ class MarketingController extends Controller return Inertia::render('marketing/Occasions', ['type' => $type]); } } + diff --git a/app/Http/Controllers/OAuthController.php b/app/Http/Controllers/OAuthController.php index 13802c4..2d35c7e 100644 --- a/app/Http/Controllers/OAuthController.php +++ b/app/Http/Controllers/OAuthController.php @@ -523,16 +523,16 @@ class OAuthController extends Controller $error = $request->get('error'); if ($error) { - return redirect('/admin')->with('error', 'Stripe connection failed: '.$error); + return redirect('/event-admin')->with('error', 'Stripe connection failed: '.$error); } if (! $code || ! $state) { - return redirect('/admin')->with('error', 'Invalid callback parameters'); + return redirect('/event-admin')->with('error', 'Invalid callback parameters'); } $sessionState = session('stripe_state'); if (! hash_equals($state, (string) $sessionState)) { - return redirect('/admin')->with('error', 'Invalid state parameter'); + return redirect('/event-admin')->with('error', 'Invalid state parameter'); } $client = new Client(); @@ -554,7 +554,7 @@ class OAuthController extends Controller $tokenData = json_decode($response->getBody()->getContents(), true); if (! isset($tokenData['stripe_user_id'])) { - return redirect('/admin')->with('error', 'Failed to connect Stripe account'); + return redirect('/event-admin')->with('error', 'Failed to connect Stripe account'); } $tenant = Tenant::find(session('tenant_id')); @@ -563,10 +563,11 @@ class OAuthController extends Controller } session()->forget(['stripe_state', 'tenant_id']); - return redirect('/admin')->with('success', 'Stripe account connected successfully'); + return redirect('/event-admin')->with('success', 'Stripe account connected successfully'); } catch (\Exception $e) { Log::error('Stripe OAuth error: '.$e->getMessage()); - return redirect('/admin')->with('error', 'Connection error: '.$e->getMessage()); + return redirect('/event-admin')->with('error', 'Connection error: '.$e->getMessage()); } } } + diff --git a/docs/prp/tenant-app-specs/api-usage.md b/docs/prp/tenant-app-specs/api-usage.md index f09b929..491fa78 100644 --- a/docs/prp/tenant-app-specs/api-usage.md +++ b/docs/prp/tenant-app-specs/api-usage.md @@ -19,7 +19,7 @@ Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit - **Response**: Neuer Access/Refresh-Token - **Token Validation**: `GET /api/v1/tenant/me` - - **Redirect URI**: Standardmaessig `${origin}/admin/auth/callback` (per Vite-Env anpassbar) + - **Redirect URI**: Standardmaessig `${origin}/event-admin/auth/callback` (per Vite-Env anpassbar) - **Headers**: `Authorization: Bearer {access_token}` - **Response**: `{ id, email, tenant_id, role, name }` @@ -272,3 +272,4 @@ curl -H "Authorization: Bearer {token}" \ Für weitere Details siehe die spezifischen Dokumentationsdateien. + diff --git a/docs/prp/tenant-app-specs/capacitor-setup.md b/docs/prp/tenant-app-specs/capacitor-setup.md index b6645b7..62c4adc 100644 --- a/docs/prp/tenant-app-specs/capacitor-setup.md +++ b/docs/prp/tenant-app-specs/capacitor-setup.md @@ -121,7 +121,7 @@ packages/mobile/ # Shared Native-Config (optional) - **Privacy**: Usage Descriptions in Info.plist (z.B. "Kamera für QR-Scans"). - **PWA-Fallback** (Web): - - **manifest.json**: `start_url: '/admin/'`, `display: 'standalone'`. + - **manifest.json**: `start_url: '/event-admin/'`, `display: 'standalone'`. - **Service Worker**: Caching von Assets; Background Sync für Mutations. - **Distribution**: Hosting auf `admin.fotospiel.app` mit A2HS-Prompt. @@ -152,4 +152,4 @@ packages/mobile/ # Shared Native-Config (optional) - **Security**: HTTPS-only; Token-Rotation alle 24h; No Jailbreak-Detection. - **Performance**: Bundle-Size < 10MB (Web-Assets komprimiert); Lazy-Loading. -Diese Setup ergänzt die funktionalen Specs und UI-Beschreibungen. Für Repo-Integration siehe ADR-0006. \ No newline at end of file +Diese Setup ergänzt die funktionalen Specs und UI-Beschreibungen. Für Repo-Integration siehe ADR-0006. diff --git a/resources/js/admin/auth/tokens.ts b/resources/js/admin/auth/tokens.ts index aa794ca..9943f03 100644 --- a/resources/js/admin/auth/tokens.ts +++ b/resources/js/admin/auth/tokens.ts @@ -1,5 +1,6 @@ import { generateCodeChallenge, generateCodeVerifier, generateState } from './pkce'; import { decodeStoredTokens } from './utils'; +import { ADMIN_AUTH_CALLBACK_PATH } from '../constants'; const TOKEN_STORAGE_KEY = 'tenant_oauth_tokens.v1'; const CODE_VERIFIER_KEY = 'tenant_oauth_code_verifier'; @@ -18,7 +19,7 @@ function getClientId(): string { } function buildRedirectUri(): string { - return new URL('/admin/auth/callback', window.location.origin).toString(); + return new URL(ADMIN_AUTH_CALLBACK_PATH, window.location.origin).toString(); } export class AuthError extends Error { diff --git a/resources/js/admin/components/AdminLayout.tsx b/resources/js/admin/components/AdminLayout.tsx index 9df0f98..fa6f87a 100644 --- a/resources/js/admin/components/AdminLayout.tsx +++ b/resources/js/admin/components/AdminLayout.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import { cn } from '@/lib/utils'; +import { ADMIN_EVENTS_PATH, ADMIN_SETTINGS_PATH } from '../constants'; const navItems = [ - { to: '/admin/events', label: 'Events' }, - { to: '/admin/settings', label: 'Einstellungen' }, + { to: ADMIN_EVENTS_PATH, label: 'Events' }, + { to: ADMIN_SETTINGS_PATH, label: 'Einstellungen' }, ]; interface AdminLayoutProps { @@ -58,3 +59,4 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP ); } + diff --git a/resources/js/admin/constants.ts b/resources/js/admin/constants.ts new file mode 100644 index 0000000..6aba634 --- /dev/null +++ b/resources/js/admin/constants.ts @@ -0,0 +1,9 @@ +export const ADMIN_BASE_PATH = '/event-admin'; + +export const adminPath = (suffix = ''): string => `${ADMIN_BASE_PATH}${suffix}`; + +export const ADMIN_HOME_PATH = ADMIN_BASE_PATH; +export const ADMIN_LOGIN_PATH = adminPath('/login'); +export const ADMIN_AUTH_CALLBACK_PATH = adminPath('/auth/callback'); +export const ADMIN_EVENTS_PATH = adminPath('/events'); +export const ADMIN_SETTINGS_PATH = adminPath('/settings'); diff --git a/resources/js/admin/pages/AuthCallbackPage.tsx b/resources/js/admin/pages/AuthCallbackPage.tsx index f32a3f5..c68afaf 100644 --- a/resources/js/admin/pages/AuthCallbackPage.tsx +++ b/resources/js/admin/pages/AuthCallbackPage.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../auth/context'; import { isAuthError } from '../auth/tokens'; +import { ADMIN_HOME_PATH } from '../constants'; export default function AuthCallbackPage() { const { completeLogin } = useAuth(); @@ -12,7 +13,7 @@ export default function AuthCallbackPage() { const params = new URLSearchParams(window.location.search); completeLogin(params) .then((redirectTo) => { - navigate(redirectTo ?? '/admin', { replace: true }); + navigate(redirectTo ?? ADMIN_HOME_PATH, { replace: true }); }) .catch((err) => { console.error('[Auth] Callback processing failed', err); @@ -33,3 +34,4 @@ export default function AuthCallbackPage() { ); } + diff --git a/resources/js/admin/pages/EventDetailPage.tsx b/resources/js/admin/pages/EventDetailPage.tsx index d57d81c..0b36f62 100644 --- a/resources/js/admin/pages/EventDetailPage.tsx +++ b/resources/js/admin/pages/EventDetailPage.tsx @@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { AdminLayout } from '../components/AdminLayout'; import { createInviteLink, getEvent, getEventStats, TenantEvent, EventStats as TenantEventStats, toggleEvent } from '../api'; import { isAuthError } from '../auth/tokens'; +import { adminPath } from '../constants'; interface State { event: TenantEvent | null; @@ -105,7 +106,7 @@ export default function EventDetailPage() { <> - Package auswählen - Wählen Sie das Package für Ihr Event. Höhere Packages bieten mehr Limits und Features. + Package auswaehlen + Waehlen Sie das Package fuer Ihr Event. Hoehere Packages bieten mehr Limits und Features.
{packages?.map((pkg) => (

{pkg.name}

-

{pkg.price} €

+

{pkg.price} EUR

  • Max Fotos: {pkg.max_photos}
  • -
  • Max Gäste: {pkg.max_guests}
  • +
  • Max Gaeste: {pkg.max_guests}
  • Galerie: {pkg.gallery_days} Tage
  • Features: {Object.keys(pkg.features).filter(k => pkg.features[k]).join(', ')}
diff --git a/resources/js/admin/pages/EventPhotosPage.tsx b/resources/js/admin/pages/EventPhotosPage.tsx index 207236e..c8faf16 100644 --- a/resources/js/admin/pages/EventPhotosPage.tsx +++ b/resources/js/admin/pages/EventPhotosPage.tsx @@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { AdminLayout } from '../components/AdminLayout'; import { deletePhoto, featurePhoto, getEventPhotos, TenantPhoto, unfeaturePhoto } from '../api'; import { isAuthError } from '../auth/tokens'; +import { adminPath } from '../constants'; export default function EventPhotosPage() { const [searchParams] = useSearchParams(); @@ -81,7 +82,7 @@ export default function EventPhotosPage() { Kein Slug in der URL gefunden. Kehre zur Event-Liste zurueck und waehle dort ein Event aus. - @@ -93,7 +94,7 @@ export default function EventPhotosPage() { const actions = (
); } + diff --git a/resources/js/admin/pages/EventsPage.tsx b/resources/js/admin/pages/EventsPage.tsx index 47d3d48..9bed757 100644 --- a/resources/js/admin/pages/EventsPage.tsx +++ b/resources/js/admin/pages/EventsPage.tsx @@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { AdminLayout } from '../components/AdminLayout'; import { getEvents, TenantEvent, getPackages } from '../api'; import { isAuthError } from '../auth/tokens'; +import { adminPath, ADMIN_SETTINGS_PATH } from '../constants'; export default function EventsPage() { const [rows, setRows] = React.useState([]); @@ -41,11 +42,11 @@ export default function EventsPage() { <> - + @@ -111,7 +112,7 @@ export default function EventsPage() { {loading ? ( ) : rows.length === 0 ? ( - navigate('/admin/events/new')} /> + navigate(adminPath('/events/new'))} /> ) : (
{rows.map((event) => ( @@ -163,15 +164,15 @@ function EventCard({ event }: { event: TenantEvent }) {
); } + diff --git a/resources/js/admin/pages/SettingsPage.tsx b/resources/js/admin/pages/SettingsPage.tsx index 1d083af..0c9669d 100644 --- a/resources/js/admin/pages/SettingsPage.tsx +++ b/resources/js/admin/pages/SettingsPage.tsx @@ -8,19 +8,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { AdminLayout } from '../components/AdminLayout'; import { useAuth } from '../auth/context'; +import { ADMIN_EVENTS_PATH, ADMIN_LOGIN_PATH } from '../constants'; export default function SettingsPage() { const navigate = useNavigate(); const { user, logout } = useAuth(); function handleLogout() { - logout({ redirect: '/admin/login' }); + logout({ redirect: ADMIN_LOGIN_PATH }); } const actions = (