From 417b1da4848d57721eb880ba6f99fad68656e867 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 8 Oct 2025 21:57:46 +0200 Subject: [PATCH] feat: Implementierung des Checkout-Logins mit E-Mail/Username-Support --- app/Http/Controllers/CheckoutController.php | 51 ++++++++++ app/Http/Controllers/LocaleController.php | 5 +- docs/prp/13-backend-authentication.md | 80 +++++++++++++-- public/lang/de/auth.json | 66 +++++++++++++ public/lang/de/common.json | 19 ++++ public/lang/de/marketing.json | 97 +++++++++++++++++-- public/lang/en/auth.json | 69 ++++++++++++- public/lang/en/common.json | 19 ++++ public/lang/en/marketing.json | 85 ++++++++++++++++ resources/js/components/delete-user.tsx | 19 ++-- resources/js/layouts/app/Header.tsx | 8 +- resources/js/pages/auth/LoginForm.tsx | 26 ++--- resources/js/pages/auth/confirm-password.tsx | 20 ++-- resources/js/pages/auth/forgot-password.tsx | 11 ++- resources/js/pages/auth/reset-password.tsx | 34 ++++--- .../marketing/checkout/CheckoutWizard.tsx | 44 +++++---- .../marketing/checkout/WizardContext.tsx | 19 +++- .../marketing/checkout/steps/AuthStep.tsx | 14 +-- .../checkout/steps/ConfirmationStep.tsx | 11 ++- .../marketing/checkout/steps/PackageStep.tsx | 12 ++- .../marketing/checkout/steps/PaymentStep.tsx | 49 +++++----- resources/js/pages/settings/password.tsx | 62 ++++++------ resources/js/pages/settings/profile.tsx | 64 ++++++------ routes/web.php | 2 + tests/Feature/Checkout/CheckoutAuthTest.php | 56 ++++++++--- 25 files changed, 730 insertions(+), 212 deletions(-) diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php index c188807..0b07b27 100644 --- a/app/Http/Controllers/CheckoutController.php +++ b/app/Http/Controllers/CheckoutController.php @@ -5,10 +5,12 @@ namespace App\Http\Controllers; use App\Models\Package; use App\Models\Tenant; use App\Models\User; +use App\Http\Controllers\Auth\AuthenticatedSessionController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rules\Password; @@ -109,6 +111,55 @@ class CheckoutController extends Controller ]); } + public function login(Request $request) + { + $validator = Validator::make($request->all(), [ + 'identifier' => 'required|string', + 'password' => 'required|string', + 'remember' => 'boolean', + 'locale' => 'nullable|string', + ]); + + if ($validator->fails()) { + return response()->json(['errors' => $validator->errors()], 422); + } + + $packageId = $request->session()->get('selected_package_id'); + + // Custom Auth für Identifier (E-Mail oder Username) + $identifier = $request->identifier; + $user = User::where('email', $identifier) + ->orWhere('username', $identifier) + ->first(); + + if (!$user || !Hash::check($request->password, $user->password)) { + return response()->json([ + 'errors' => ['identifier' => ['Ungültige Anmeldedaten.']] + ], 422); + } + + Auth::login($user, $request->boolean('remember')); + $request->session()->regenerate(); + + // Checkout-spezifische Logik + DB::transaction(function () use ($request, $user, $packageId) { + if ($packageId && !$user->pending_purchase) { + $user->update(['pending_purchase' => true]); + $request->session()->put('pending_package_id', $packageId); + } + }); + + return response()->json([ + 'user' => [ + 'id' => $user->id, + 'email' => $user->email, + 'name' => $user->name ?? null, + 'pending_purchase' => $user->pending_purchase ?? false, + ], + 'message' => 'Login erfolgreich', + ]); + } + public function createPaymentIntent(Request $request) { $request->validate([ diff --git a/app/Http/Controllers/LocaleController.php b/app/Http/Controllers/LocaleController.php index 503cc61..65726e6 100644 --- a/app/Http/Controllers/LocaleController.php +++ b/app/Http/Controllers/LocaleController.php @@ -19,9 +19,6 @@ class LocaleController extends Controller } // Return JSON response for fetch requests - return response()->json([ - 'success' => true, - 'locale' => App::getLocale(), - ]); + return back(); } } \ No newline at end of file diff --git a/docs/prp/13-backend-authentication.md b/docs/prp/13-backend-authentication.md index 1c8337b..519df28 100644 --- a/docs/prp/13-backend-authentication.md +++ b/docs/prp/13-backend-authentication.md @@ -2,9 +2,61 @@ ## Overview -This document outlines the authentication requirements and implementation details for the Fotospiel tenant backend. The system uses OAuth2 with PKCE (Proof Key for Code Exchange) for secure authorization, providing tenant-specific access tokens for API operations. +This document outlines the authentication requirements and implementation details for the Fotospiel tenant backend. The system uses OAuth2 with PKCE (Proof Key for Code Exchange) for secure authorization, providing tenant-specific access tokens for API operations. Additionally, session-based authentication is used for web interfaces like the checkout wizard, supporting both email and username login. -## Authentication Flow +## Session-Based Authentication (Web/Checkout) + +### Checkout Login Flow +- **Endpoint**: `POST /checkout/login` +- **Method**: POST +- **Content-Type**: `application/json` +- **Parameters**: + - `identifier`: Email or username (required, string) + - `password`: User's password (required, string) + - `remember`: Remember me flag (optional, boolean) + - `locale`: Language locale (optional, string, e.g., 'de') + +**Authentication Logic**: +- Validate input using Laravel Validator. +- Search for user by email or username using Eloquent query: `User::where('email', $identifier)->orWhere('username', $identifier)->first()`. +- Verify password with `Hash::check()`. +- If valid, log in user with `Auth::login($user, $remember)` and regenerate session. +- Set `pending_purchase = true` if a package is selected (from session) and not already set, wrapped in DB transaction. +- Return JSON response with user data for AJAX handling in frontend. + +**Response** (JSON, 200 OK): +```json +{ + "user": { + "id": 1, + "email": "user@example.com", + "name": "John Doe", + "pending_purchase": true + }, + "message": "Login erfolgreich" +} +``` + +**Error Response** (JSON, 422 Unprocessable Entity): +```json +{ + "errors": { + "identifier": ["Ungültige Anmeldedaten."] + } +} +``` + +**Security**: +- CSRF protection via `web` middleware. +- Rate limiting recommended (add `throttle:6,1` middleware). +- Password hashing with Laravel's `Hash` facade. +- Session regeneration after login to prevent fixation attacks. + +### Integration with Standard Laravel Auth +- Leverages `AuthenticatedSessionController` for core logic where possible, but custom handling for identifier flexibility and checkout context. +- Compatible with Inertia.js for SPA responses. + +## OAuth2 Authentication (API) ### 1. Authorization Request - **Endpoint**: `GET /api/v1/oauth/authorize` @@ -114,6 +166,7 @@ CREATE TABLE oauth_clients ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT oauth_clients_tenant_id_foreign FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE SET NULL ); +``` ```sql CREATE TABLE oauth_clients ( id VARCHAR(255) PRIMARY KEY, @@ -159,6 +212,7 @@ CREATE TABLE refresh_tokens ( revoked_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +``` ```sql CREATE TABLE refresh_tokens ( id VARCHAR(255) PRIMARY KEY, @@ -201,6 +255,7 @@ CREATE TABLE tenant_tokens ( | `/oauth/authorize` | GET | Authorization request | None | | `/oauth/token` | POST | Token exchange/refresh | None | | `/api/v1/tenant/me` | GET | Validate token | Bearer Token | +| `/checkout/login` | POST | Session login for checkout (email/username) | None | ### Protected Endpoints All tenant API endpoints require `Authorization: Bearer {access_token}` header. @@ -274,6 +329,13 @@ VITE_OAUTH_CLIENT_ID=tenant-admin-app "error": "insufficient_scope", "error_description": "Scope tenant:write required" } + +// 422 Unprocessable Entity (Checkout Login) +{ + "errors": { + "identifier": ["Ungültige Anmeldedaten."] + } +} ``` ## Implementation Notes @@ -296,12 +358,14 @@ VITE_OAUTH_CLIENT_ID=tenant-admin-app - Authorization requests: 10/minute per IP - Token exchanges: 5/minute per IP - Token validation: 100/minute per tenant +- Checkout login: 6/minute per IP (add throttle middleware) ### 5. Logging and Monitoring - Log all authentication attempts (success/failure) - Monitor token usage patterns - Alert on unusual activity (multiple failed attempts, token anomalies) - Track refresh token usage for security analysis +- Log checkout login attempts with identifier type (email/username) ### 6. Database Cleanup - Cron job to remove expired authorization codes (daily) @@ -315,18 +379,21 @@ VITE_OAUTH_CLIENT_ID=tenant-admin-app - State parameter security - Token signing and verification - Scope validation middleware +- Checkout login with email and username ### Integration Tests - Complete OAuth2 flow (authorize → token → validate) - Token refresh cycle - Error scenarios (invalid code, expired tokens, state mismatch) - Concurrent access testing +- Checkout login flow with pending_purchase ### Security Tests - CSRF protection validation - PKCE bypass attempts - Token replay attacks - Rate limiting enforcement +- Username/email ambiguity handling ## Deployment Considerations @@ -345,11 +412,6 @@ VITE_OAUTH_CLIENT_ID=tenant-admin-app - Monitor token expiry patterns - Alert on PKCE validation failures - Log all security-related events +- Monitor checkout login success rates and identifier usage -This implementation provides secure, scalable authentication for the Fotospiel tenant system, following OAuth2 best practices with PKCE for public clients. - - - - - - +This implementation provides secure, scalable authentication for the Fotospiel tenant system, following OAuth2 best practices with PKCE for public clients and flexible session auth for web flows. diff --git a/public/lang/de/auth.json b/public/lang/de/auth.json index af6673d..25968d3 100644 --- a/public/lang/de/auth.json +++ b/public/lang/de/auth.json @@ -20,8 +20,12 @@ "failed_credentials": "Falsche Anmeldedaten.", "login": { "title": "Anmelden", + "identifier": "E-Mail oder Username", + "identifier_placeholder": "Geben Sie Ihre E-Mail oder Ihren Username ein", "username_or_email": "Username oder E-Mail", "password": "Passwort", + "password_placeholder": "Geben Sie Ihr Passwort ein", + "forgot": "Passwort vergessen?", "remember": "Angemeldet bleiben", "submit": "Anmelden" }, @@ -64,6 +68,68 @@ "privacy_consent": "Datenschutz-Zustimmung" } }, + "common": { + "ui": { + "language_select": "Sprache auswählen" + } + }, + "settings": { + "profile": { + "title": "Profil-Einstellungen", + "section_title": "Profilinformationen", + "description": "Aktualisieren Sie Ihren Namen und Ihre E-Mail-Adresse", + "email": "E-Mail-Adresse", + "email_placeholder": "E-Mail-Adresse", + "username": "Username", + "username_placeholder": "Username", + "language": "Sprache", + "email_unverified": "Ihre E-Mail-Adresse ist nicht verifiziert.", + "resend_verification": "Klicken Sie hier, um die Verifizierungs-E-Mail erneut zu senden.", + "verification_sent": "Ein neuer Verifizierungslink wurde an Ihre E-Mail-Adresse gesendet." + }, + "password": { + "title": "Passwort-Einstellungen", + "section_title": "Passwort aktualisieren", + "description": "Stellen Sie sicher, dass Ihr Konto ein langes, zufälliges Passwort verwendet, um sicher zu bleiben", + "current": "Aktuelles Passwort", + "current_placeholder": "Aktuelles Passwort", + "new": "Neues Passwort", + "new_placeholder": "Neues Passwort", + "confirm": "Passwort bestätigen", + "confirm_placeholder": "Passwort bestätigen", + "save_button": "Passwort speichern" + } + }, + "reset": { + "password": "Passwort", + "password_placeholder": "Passwort", + "confirm_password": "Passwort bestätigen", + "confirm_password_placeholder": "Passwort bestätigen", + "email": "E-Mail", + "email_placeholder": "email@beispiel.de", + "title": "Passwort zurücksetzen", + "description": "Bitte geben Sie Ihr neues Passwort unten ein", + "submit": "Passwort zurücksetzen" + }, + "confirm": { + "password": "Passwort", + "password_placeholder": "Passwort", + "confirm": "Passwort bestätigen", + "title": "Passwort bestätigen", + "description": "Dies ist ein sicherer Bereich der Anwendung. Bitte bestätigen Sie Ihr Passwort, bevor Sie fortfahren.", + "submit": "Passwort bestätigen" + }, + "forgot": { + "email": "E-Mail-Adresse", + "email_placeholder": "email@beispiel.de", + "submit": "Passwort-Reset-Link per E-Mail senden", + "back": "Oder zurück zur Anmeldung" + }, + "delete_user": { + "password": "Passwort", + "password_placeholder": "Passwort", + "confirm_text": "Sobald Ihr Konto gelöscht ist, werden alle seine Ressourcen und Daten auch dauerhaft gelöscht. Bitte geben Sie Ihr Passwort ein, um zu bestätigen, dass Sie Ihr Konto dauerhaft löschen möchten." + }, "verification": { "notice": "Bitte bestätigen Sie Ihre E-Mail-Adresse.", "resend": "E-Mail erneut senden" diff --git a/public/lang/de/common.json b/public/lang/de/common.json index 5dcca71..7e2b7e5 100644 --- a/public/lang/de/common.json +++ b/public/lang/de/common.json @@ -31,5 +31,24 @@ "lisa": { "name": "Lisa K." } + }, + "ui": { + "saved": "Gespeichert", + "loading": "Laden...", + "error": "Fehler", + "success": "Erfolg", + "please_enter": "Bitte eingeben", + "click_here": "Hier klicken", + "resend_verification": "Verifizierungs-E-Mail erneut senden", + "verify_email": "E-Mail verifizieren", + "check_email": "Überprüfen Sie Ihre E-Mail auf den Verifizierungslink.", + "login_to_continue": "Melden Sie sich an, um fortzufahren.", + "no_account": "Kein Konto? Registrieren", + "already_registered": "Bereits registriert? Anmelden", + "redirecting": "Weiterleitung zum Admin-Bereich...", + "complete_purchase": "Kauf abschließen", + "email_verify_title": "E-Mail verifizieren", + "email_verify_desc": "Bitte überprüfen Sie Ihre E-Mail und klicken Sie auf den Verifizierungslink.", + "language_select": "Sprache wählen" } } \ No newline at end of file diff --git a/public/lang/de/marketing.json b/public/lang/de/marketing.json index c639678..9e36f6f 100644 --- a/public/lang/de/marketing.json +++ b/public/lang/de/marketing.json @@ -1,7 +1,11 @@ { "header": { - "login:" : "Anmelden", - "register": "Registrieren" + "login": "Anmelden", + "register": "Registrieren", + "home": "Startseite", + "packages": "Pakete", + "blog": "Blog", + "contact": "Kontakt" }, "home": { "title": "Startseite - Fotospiel", @@ -129,16 +133,16 @@ "what_customers_say": "Was unsere Kunden sagen", "close": "Schließen", "to_order": "Jetzt bestellen", - "currency": { - "euro": "€" - }, - "view_details": "Details ansehen", - "feature": "Feature", "details": "Details", "customer_opinions": "Kundenmeinungen", "errors": { "select_package": "Bitte wählen Sie ein Paket aus." - } + }, + "currency": { + "euro": "€" + }, + "view_details": "Details ansehen", + "feature": "Feature" }, "blog": { "title": "Fotospiel - Blog", @@ -337,5 +341,82 @@ "name": "Lisa K." } } + }, + "checkout": { + "title": "Checkout", + "subtitle": "Sicheren Kaufprozess", + "back": "Zurück", + "next": "Weiter", + "cancel": "Abbrechen", + "package_step": { + "title": "Paket wählen", + "subtitle": "Auswahl und Vergleich", + "description": "Wähle das passende Paket für deine Bedürfnisse", + "no_package_selected": "Kein Paket ausgewählt. Bitte wähle ein Paket aus der Paketübersicht.", + "alternatives_title": "Alternative Pakete", + "no_alternatives": "Keine weiteren Pakete in dieser Kategorie verfügbar.", + "next_to_account": "Weiter zum Konto", + "loading": "Wird geladen..." + }, + "auth_step": { + "title": "Konto", + "subtitle": "Anmelden oder Registrieren", + "description": "Erstellen Sie ein Konto oder melden Sie sich an, um mit dem Kauf fortzufahren.", + "already_logged_in_title": "Bereits eingeloggt", + "already_logged_in_desc": "Sie sind bereits als {email} eingeloggt.", + "next_to_payment": "Weiter zur Zahlung", + "switch_to_register": "Registrieren", + "switch_to_login": "Anmelden", + "google_coming_soon": "Google-Login kommt bald im Comfort-Delta." + }, + "payment_step": { + "title": "Zahlung", + "subtitle": "Sichere Zahlung", + "description": "Schließen Sie Ihren Kauf sicher mit Ihrer gewählten Zahlungsmethode ab.", + "free_package_title": "Kostenloses Paket", + "free_package_desc": "Dieses Paket ist kostenlos. Wir aktivieren es direkt nach der Bestätigung.", + "activate_package": "Paket aktivieren", + "loading_payment": "Zahlungsdaten werden geladen...", + "secure_payment_desc": "Sichere Zahlung mit Kreditkarte, Debitkarte oder SEPA-Lastschrift.", + "payment_failed": "Zahlung fehlgeschlagen. ", + "error_card": "Kartenfehler aufgetreten.", + "error_validation": "Eingabedaten sind ungültig.", + "error_connection": "Verbindungsfehler. Bitte Internetverbindung prüfen.", + "error_server": "Serverfehler. Bitte später erneut versuchen.", + "error_auth": "Authentifizierungsfehler. Bitte Seite neu laden.", + "error_unknown": "Unbekannter Fehler aufgetreten.", + "processing": "Zahlung wird verarbeitet. Bitte warten...", + "needs_method": "Zahlungsmethode wird benötigt. Bitte Kartendaten überprüfen.", + "needs_confirm": "Zahlung muss bestätigt werden.", + "unexpected_status": "Unerwarteter Zahlungsstatus: {status}", + "processing_btn": "Verarbeitung...", + "pay_now": "Jetzt bezahlen (€{price})", + "stripe_not_loaded": "Stripe ist nicht initialisiert. Bitte Seite neu laden.", + "network_error": "Netzwerkfehler beim Laden der Zahlungsdaten", + "payment_intent_error": "Fehler beim Laden der Zahlungsdaten" + }, + "confirmation_step": { + "title": "Bestätigung", + "subtitle": "Alles erledigt!", + "description": "Ihr Paket ist aktiviert. Überprüfen Sie Ihre E-Mail für Details.", + "welcome": "Willkommen bei Fotospiel", + "package_activated": "Ihr Paket '{name}' ist aktiviert.", + "email_sent": "Wir haben Ihnen eine Bestätigungs-E-Mail gesendet.", + "open_profile": "Profil öffnen", + "to_admin": "Zum Admin-Bereich" + }, + "confirmation": { + "welcome": "Willkommen bei Fotospiel", + "package_activated": "Ihr Paket '{name}' ist aktiviert.", + "email_sent": "Wir haben Ihnen eine Bestätigungs-E-Mail gesendet.", + "open_profile": "Profil öffnen", + "to_admin": "Zum Admin-Bereich" + }, + "auth": { + "already_logged_in": "Sie sind bereits als {email} eingeloggt.", + "switch_to_register": "Registrieren", + "switch_to_login": "Anmelden", + "google_coming_soon": "Google-Login kommt bald im Comfort-Delta." + } } } \ No newline at end of file diff --git a/public/lang/en/auth.json b/public/lang/en/auth.json index 8a50f57..35ebd0f 100644 --- a/public/lang/en/auth.json +++ b/public/lang/en/auth.json @@ -20,8 +20,13 @@ "failed_credentials": "Invalid credentials.", "login": { "title": "Login", - "username_or_email": "Username or Email", + "identifier": "Email or Username", + "identifier_placeholder": "Enter your email or username", + "email": "Email", + "email_placeholder": "Enter your email", "password": "Password", + "password_placeholder": "Enter your password", + "forgot": "Forgot Password?", "remember": "Remember me", "submit": "Login" }, @@ -64,6 +69,68 @@ "privacy_consent": "Privacy Consent" } }, + "common": { + "ui": { + "language_select": "Select Language" + } + }, + "settings": { + "profile": { + "title": "Profile settings", + "section_title": "Profile information", + "description": "Update your name and email address", + "email": "Email address", + "email_placeholder": "Email address", + "username": "Username", + "username_placeholder": "Username", + "language": "Language", + "email_unverified": "Your email address is unverified.", + "resend_verification": "Click here to resend the verification email.", + "verification_sent": "A new verification link has been sent to your email address." + }, + "password": { + "title": "Password settings", + "section_title": "Update password", + "description": "Ensure your account is using a long, random password to stay secure", + "current": "Current password", + "current_placeholder": "Current password", + "new": "New password", + "new_placeholder": "New password", + "confirm": "Confirm password", + "confirm_placeholder": "Confirm password", + "save_button": "Save password" + } + }, + "reset": { + "password": "Password", + "password_placeholder": "Password", + "confirm_password": "Confirm password", + "confirm_password_placeholder": "Confirm password", + "email": "Email", + "email_placeholder": "email@example.com", + "title": "Reset password", + "description": "Please enter your new password below", + "submit": "Reset password" + }, + "confirm": { + "password": "Password", + "password_placeholder": "Password", + "confirm": "Confirm password", + "title": "Confirm your password", + "description": "This is a secure area of the application. Please confirm your password before continuing.", + "submit": "Confirm password" + }, + "forgot": { + "email": "Email address", + "email_placeholder": "email@example.com", + "submit": "Email password reset link", + "back": "Or, return to log in" + }, + "delete_user": { + "password": "Password", + "password_placeholder": "Password", + "confirm_text": "Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete your account." + }, "verification": { "notice": "Please confirm your email address.", "resend": "Resend email" diff --git a/public/lang/en/common.json b/public/lang/en/common.json index 9105a23..dc8deb2 100644 --- a/public/lang/en/common.json +++ b/public/lang/en/common.json @@ -31,5 +31,24 @@ "lisa": { "name": "Lisa K." } + }, + "ui": { + "saved": "Saved", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "please_enter": "Please enter", + "click_here": "Click here", + "resend_verification": "Resend verification email", + "verify_email": "Verify Email", + "check_email": "Check your email for the verification link.", + "login_to_continue": "Log in to continue.", + "no_account": "No Account? Register", + "already_registered": "Already registered? Log in", + "redirecting": "Redirecting to Admin Area...", + "complete_purchase": "Complete Purchase", + "email_verify_title": "Verify Email", + "email_verify_desc": "Please check your email and click the verification link.", + "language_select": "Language Select" } } \ No newline at end of file diff --git a/public/lang/en/marketing.json b/public/lang/en/marketing.json index f8172fb..1de238e 100644 --- a/public/lang/en/marketing.json +++ b/public/lang/en/marketing.json @@ -279,6 +279,14 @@ "privacy": "Privacy", "impressum": "Imprint" }, + "header": { + "home": "Home", + "packages": "Packages", + "blog": "Blog", + "contact": "Contact", + "login": "Login", + "register": "Register" + }, "footer": { "company": "Fotospiel GmbH", "rights_reserved": "All Rights Reserved" @@ -324,5 +332,82 @@ "name": "Lisa K." } } + }, + "checkout": { + "title": "Checkout", + "subtitle": "Secure Checkout Process", + "back": "Back", + "next": "Next", + "cancel": "Cancel", + "package_step": { + "title": "Select Package", + "subtitle": "Selection and Comparison", + "description": "Choose the right package for your needs", + "no_package_selected": "No package selected. Please choose a package from the overview.", + "alternatives_title": "Alternative Packages", + "no_alternatives": "No further packages in this category available.", + "next_to_account": "Next to Account", + "loading": "Loading..." + }, + "auth_step": { + "title": "Account", + "subtitle": "Login or Register", + "description": "Create an account or log in to continue with your purchase.", + "already_logged_in_title": "Already Logged In", + "already_logged_in_desc": "You are already logged in as {email}.", + "next_to_payment": "Next to Payment", + "switch_to_register": "Register", + "switch_to_login": "Login", + "google_coming_soon": "Google Login coming soon in Comfort-Delta." + }, + "payment_step": { + "title": "Payment", + "subtitle": "Secure Payment", + "description": "Complete your purchase securely with your chosen payment method.", + "free_package_title": "Free Package", + "free_package_desc": "This package is free. We activate it directly after confirmation.", + "activate_package": "Activate Package", + "loading_payment": "Payment data is loading...", + "secure_payment_desc": "Secure payment with credit card, debit card or SEPA direct debit.", + "payment_failed": "Payment failed. ", + "error_card": "Card error occurred.", + "error_validation": "Input data is invalid.", + "error_connection": "Connection error. Please check your internet connection.", + "error_server": "Server error. Please try again later.", + "error_auth": "Authentication error. Please reload the page.", + "error_unknown": "Unknown error occurred.", + "processing": "Payment is being processed. Please wait...", + "needs_method": "Payment method required. Please check card details.", + "needs_confirm": "Payment needs confirmation.", + "unexpected_status": "Unexpected payment status: {status}", + "processing_btn": "Processing...", + "pay_now": "Pay Now (${price})", + "stripe_not_loaded": "Stripe is not initialized. Please reload the page.", + "network_error": "Network error loading payment data", + "payment_intent_error": "Error loading payment data" + }, + "confirmation_step": { + "title": "Confirmation", + "subtitle": "All Done!", + "description": "Your package is activated. Check your email for details.", + "welcome": "Welcome to FotoSpiel", + "package_activated": "Your package '{name}' is activated.", + "email_sent": "We have sent you a confirmation email.", + "open_profile": "Open Profile", + "to_admin": "To Admin Area" + }, + "confirmation": { + "welcome": "Welcome to FotoSpiel", + "package_activated": "Your package '{name}' is activated.", + "email_sent": "We have sent you a confirmation email.", + "open_profile": "Open Profile", + "to_admin": "To Admin Area" + }, + "auth": { + "already_logged_in": "You are already logged in as {email}.", + "switch_to_register": "Register", + "switch_to_login": "Login", + "google_coming_soon": "Google Login coming soon in Comfort-Delta." + } } } \ No newline at end of file diff --git a/resources/js/components/delete-user.tsx b/resources/js/components/delete-user.tsx index 9ca7d1c..822fe8f 100644 --- a/resources/js/components/delete-user.tsx +++ b/resources/js/components/delete-user.tsx @@ -7,9 +7,11 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Form } from '@inertiajs/react'; import { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; export default function DeleteUser() { - const passwordInput = useRef(null); + const { t } = useTranslation('auth'); + const passwordInput = useRef(null); return (
@@ -27,8 +29,7 @@ export default function DeleteUser() { Are you sure you want to delete your account? - Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password - to confirm you would like to permanently delete your account. + {t('auth.delete_user.confirm_text')}
diff --git a/resources/js/layouts/app/Header.tsx b/resources/js/layouts/app/Header.tsx index e12b147..335a073 100644 --- a/resources/js/layouts/app/Header.tsx +++ b/resources/js/layouts/app/Header.tsx @@ -24,7 +24,7 @@ const Header: React.FC = () => { }; const handleLanguageChange = useCallback(async (value: string) => { - console.log('handleLanguageChange called with:', value); + //console.log('handleLanguageChange called with:', value); try { const response = await fetch('/set-locale', { method: 'POST', @@ -35,9 +35,9 @@ const Header: React.FC = () => { body: JSON.stringify({ locale: value }), }); - console.log('fetch response:', response.status); + //console.log('fetch response:', response.status); if (response.ok) { - console.log('calling i18n.changeLanguage with:', value); + //console.log('calling i18n.changeLanguage with:', value); i18n.changeLanguage(value); // Reload only the locale prop to update the page props router.reload({ only: ['locale'] }); @@ -117,7 +117,7 @@ const Header: React.FC = () => { updateValue("email", event.target.value)} + placeholder={t("login.identifier_placeholder") || t("login.email_placeholder")} + value={values.identifier} + onChange={(event) => updateValue("identifier", event.target.value)} /> - +
- +
@@ -180,7 +180,7 @@ export default function LoginForm({ onSuccess, canResetPassword = true, locale } />
- +
- +
diff --git a/resources/js/pages/auth/forgot-password.tsx b/resources/js/pages/auth/forgot-password.tsx index b08a9c6..6c0c09e 100644 --- a/resources/js/pages/auth/forgot-password.tsx +++ b/resources/js/pages/auth/forgot-password.tsx @@ -12,9 +12,10 @@ import { Label } from '@/components/ui/label'; import AuthLayout from '@/layouts/auth-layout'; export default function ForgotPassword({ status }: { status?: string }) { - return ( - - + const { t } = useTranslation('auth'); + return ( + + {status &&
{status}
} @@ -24,7 +25,7 @@ export default function ForgotPassword({ status }: { status?: string }) { <>
- +
@@ -41,7 +42,7 @@ export default function ForgotPassword({ status }: { status?: string }) {
Or, return to - log in + {t('auth.forgot.back')}
diff --git a/resources/js/pages/auth/reset-password.tsx b/resources/js/pages/auth/reset-password.tsx index e24d50b..40ba2d0 100644 --- a/resources/js/pages/auth/reset-password.tsx +++ b/resources/js/pages/auth/reset-password.tsx @@ -1,6 +1,7 @@ import { store } from '@/actions/App/Http/Controllers/Auth/NewPasswordController'; import { Form, Head } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import InputError from '@/components/input-error'; import { Button } from '@/components/ui/button'; @@ -14,25 +15,26 @@ interface ResetPasswordProps { } export default function ResetPassword({ token, email }: ResetPasswordProps) { + const { t } = useTranslation('auth'); return ( - - + + ({ ...data, token, email })} resetOnSuccess={['password', 'password_confirmation']} > {({ processing, errors }) => (
- - - + + +
- +
- +
)} diff --git a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx index 559f6c9..534c24b 100644 --- a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx +++ b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from "react"; +import { useTranslation } from 'react-i18next'; import { Steps } from "@/components/ui/Steps"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; @@ -23,36 +24,45 @@ interface CheckoutWizardProps { initialStep?: CheckoutStepId; } -const stepConfig: { id: CheckoutStepId; title: string; description: string; details: string }[] = [ +const baseStepConfig: { id: CheckoutStepId; titleKey: string; descriptionKey: string; detailsKey: string }[] = [ { id: "package", - title: "Paket wählen", - description: "Auswahl und Vergleich", - details: "Wähle das passende Paket für deine Bedürfnisse" + titleKey: 'checkout.package_step.title', + descriptionKey: 'checkout.package_step.subtitle', + detailsKey: 'checkout.package_step.description' }, { id: "auth", - title: "Konto einrichten", - description: "Login oder Registrierung", - details: "Erstelle ein Konto oder melde dich an" + titleKey: 'checkout.auth_step.title', + descriptionKey: 'checkout.auth_step.subtitle', + detailsKey: 'checkout.auth_step.description' }, { id: "payment", - title: "Bezahlung", - description: "Sichere Zahlung", - details: "Gib deine Zahlungsdaten ein" + titleKey: 'checkout.payment_step.title', + descriptionKey: 'checkout.payment_step.subtitle', + detailsKey: 'checkout.payment_step.description' }, { id: "confirmation", - title: "Fertig!", - description: "Zugang aktiv", - details: "Dein Paket ist aktiviert" + titleKey: 'checkout.confirmation_step.title', + descriptionKey: 'checkout.confirmation_step.subtitle', + detailsKey: 'checkout.confirmation_step.description' }, ]; - const WizardBody: React.FC<{ stripePublishableKey: string; privacyHtml: string }> = ({ stripePublishableKey, privacyHtml }) => { + const { t } = useTranslation('marketing'); const { currentStep, nextStep, previousStep } = useCheckoutWizard(); + const stepConfig = useMemo(() => + baseStepConfig.map(step => ({ + id: step.id, + title: t(step.titleKey), + description: t(step.descriptionKey), + details: t(step.detailsKey), + })), + [t] + ); const currentIndex = useMemo(() => stepConfig.findIndex((step) => step.id === currentStep), [currentStep]); const progress = useMemo(() => { @@ -60,7 +70,7 @@ const WizardBody: React.FC<{ stripePublishableKey: string; privacyHtml: string } return 0; } return (currentIndex / (stepConfig.length - 1)) * 100; - }, [currentIndex]); + }, [currentIndex, stepConfig]); return (
@@ -78,10 +88,10 @@ const WizardBody: React.FC<{ stripePublishableKey: string; privacyHtml: string }
diff --git a/resources/js/pages/marketing/checkout/WizardContext.tsx b/resources/js/pages/marketing/checkout/WizardContext.tsx index 07831dd..fdb37d3 100644 --- a/resources/js/pages/marketing/checkout/WizardContext.tsx +++ b/resources/js/pages/marketing/checkout/WizardContext.tsx @@ -124,14 +124,18 @@ export function CheckoutWizardProvider({ if (savedState) { try { const parsed = JSON.parse(savedState); - // Restore state selectively - if (parsed.selectedPackage) dispatch({ type: 'SELECT_PACKAGE', payload: parsed.selectedPackage }); - if (parsed.currentStep) dispatch({ type: 'GO_TO_STEP', payload: parsed.currentStep }); + if (parsed.selectedPackage && initialPackage && parsed.selectedPackage.id === initialPackage.id && parsed.currentStep !== 'confirmation') { + // Restore state selectively + if (parsed.selectedPackage) dispatch({ type: 'SELECT_PACKAGE', payload: parsed.selectedPackage }); + if (parsed.currentStep) dispatch({ type: 'GO_TO_STEP', payload: parsed.currentStep }); + } else { + localStorage.removeItem('checkout-wizard-state'); + } } catch (error) { console.error('Failed to restore checkout state:', error); } } - }, []); + }, [initialPackage]); // Save state to localStorage whenever it changes useEffect(() => { @@ -141,6 +145,13 @@ export function CheckoutWizardProvider({ })); }, [state.selectedPackage, state.currentStep]); + // Clear localStorage when confirmation step is reached + useEffect(() => { + if (state.currentStep === 'confirmation') { + localStorage.removeItem('checkout-wizard-state'); + } + }, [state.currentStep]); + const selectPackage = useCallback((pkg: CheckoutPackage) => { dispatch({ type: 'SELECT_PACKAGE', payload: pkg }); }, []); diff --git a/resources/js/pages/marketing/checkout/steps/AuthStep.tsx b/resources/js/pages/marketing/checkout/steps/AuthStep.tsx index 35fabf5..3d0ac3d 100644 --- a/resources/js/pages/marketing/checkout/steps/AuthStep.tsx +++ b/resources/js/pages/marketing/checkout/steps/AuthStep.tsx @@ -5,12 +5,14 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { useCheckoutWizard } from "../WizardContext"; import LoginForm, { AuthUserPayload } from "../../../auth/LoginForm"; import RegisterForm, { RegisterSuccessPayload } from "../../../auth/RegisterForm"; +import { useTranslation } from 'react-i18next'; interface AuthStepProps { privacyHtml: string; } export const AuthStep: React.FC = ({ privacyHtml }) => { + const { t } = useTranslation('marketing'); const page = usePage<{ locale?: string }>(); const locale = page.props.locale ?? "de"; const { isAuthenticated, authUser, setAuthUser, nextStep, selectedPackage } = useCheckoutWizard(); @@ -48,14 +50,14 @@ export const AuthStep: React.FC = ({ privacyHtml }) => { return (
- Bereits eingeloggt + {t('checkout.auth_step.already_logged_in_title')} - {authUser.email ? `Sie sind als ${authUser.email} angemeldet.` : "Sie sind bereits angemeldet."} + {t('checkout.auth_step.already_logged_in_desc', { email: authUser?.email || '' })}
@@ -69,16 +71,16 @@ export const AuthStep: React.FC = ({ privacyHtml }) => { variant={mode === 'register' ? 'default' : 'outline'} onClick={() => setMode('register')} > - Registrieren + {t('checkout.auth_step.switch_to_register')} - Google Login folgt im Komfort-Delta. + {t('checkout.auth_step.google_coming_soon')} diff --git a/resources/js/pages/marketing/checkout/steps/ConfirmationStep.tsx b/resources/js/pages/marketing/checkout/steps/ConfirmationStep.tsx index 681c57b..1aeba8b 100644 --- a/resources/js/pages/marketing/checkout/steps/ConfirmationStep.tsx +++ b/resources/js/pages/marketing/checkout/steps/ConfirmationStep.tsx @@ -2,27 +2,30 @@ import React from "react"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { useCheckoutWizard } from "../WizardContext"; +import { useTranslation } from 'react-i18next'; interface ConfirmationStepProps { onViewProfile?: () => void; } export const ConfirmationStep: React.FC = ({ onViewProfile }) => { + const { t } = useTranslation('marketing'); const { selectedPackage } = useCheckoutWizard(); return (
- Willkommen bei FotoSpiel + {t('checkout.confirmation_step.welcome')} - Ihr Paket "{selectedPackage.name}" ist aktiviert. Wir haben Ihnen eine Bestätigung per E-Mail gesendet. + {t('checkout.confirmation_step.package_activated', { name: selectedPackage?.name || '' })} + {t('checkout.confirmation_step.email_sent')}
- +
); diff --git a/resources/js/pages/marketing/checkout/steps/PackageStep.tsx b/resources/js/pages/marketing/checkout/steps/PackageStep.tsx index c65346b..d76f65d 100644 --- a/resources/js/pages/marketing/checkout/steps/PackageStep.tsx +++ b/resources/js/pages/marketing/checkout/steps/PackageStep.tsx @@ -1,4 +1,5 @@ import React, { useMemo, useState } from "react"; +import { useTranslation } from 'react-i18next'; import { Check, Package as PackageIcon, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -77,6 +78,7 @@ function PackageOption({ pkg, isActive, onSelect }: { pkg: CheckoutPackage; isAc } export const PackageStep: React.FC = () => { + const { t } = useTranslation('marketing'); const { selectedPackage, packageOptions, setSelectedPackage, resetPaymentState, nextStep } = useCheckoutWizard(); const [isLoading, setIsLoading] = useState(false); @@ -85,7 +87,7 @@ export const PackageStep: React.FC = () => { if (!selectedPackage) { return (
-

Kein Paket ausgewählt. Bitte wähle ein Paket aus der Paketübersicht.

+

{t('checkout.package_step.no_package_selected')}

); } @@ -129,17 +131,17 @@ export const PackageStep: React.FC = () => { {isLoading ? ( <> - Wird geladen... + {t('checkout.package_step.loading')} ) : ( - "Weiter zum Konto" + t('checkout.package_step.next_to_account') )}