From f3c44be76d1cdcdc9d06d1f588636d38ae21d360 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 7 Nov 2025 13:52:29 +0100 Subject: [PATCH] fixes login page in tenant admin pwa --- .../Api/Tenant/SettingsController.php | 40 ++++++++++-- .../Api/Tenant/TenantAdminTokenController.php | 8 +-- resources/js/admin/auth/context.tsx | 4 ++ .../js/admin/components/DevTenantSwitcher.tsx | 2 +- resources/js/admin/i18n/locales/de/auth.json | 6 +- resources/js/admin/i18n/locales/en/auth.json | 6 +- resources/js/admin/pages/LoginPage.tsx | 20 ++++-- tests/Feature/Auth/TenantProfileApiTest.php | 27 ++++++++ .../Settings/NotificationPreferencesTest.php | 64 +++++++++++++++++++ 9 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 tests/Feature/Tenant/Settings/NotificationPreferencesTest.php diff --git a/app/Http/Controllers/Api/Tenant/SettingsController.php b/app/Http/Controllers/Api/Tenant/SettingsController.php index 7bfab31..6d65da5 100644 --- a/app/Http/Controllers/Api/Tenant/SettingsController.php +++ b/app/Http/Controllers/Api/Tenant/SettingsController.php @@ -8,6 +8,7 @@ use App\Http\Requests\Tenant\SettingsStoreRequest; use App\Models\Tenant; use App\Services\Packages\TenantNotificationPreferences; use App\Support\ApiError; +use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; @@ -19,7 +20,7 @@ class SettingsController extends Controller */ public function index(Request $request): JsonResponse { - $tenant = $request->tenant; + $tenant = $this->resolveTenant($request); return response()->json([ 'message' => 'Settings erfolgreich abgerufen.', @@ -35,7 +36,7 @@ class SettingsController extends Controller Request $request, TenantNotificationPreferences $preferencesService ): JsonResponse { - $tenant = $request->tenant; + $tenant = $this->resolveTenant($request); $defaults = TenantNotificationPreferences::defaults(); $resolved = []; @@ -60,7 +61,7 @@ class SettingsController extends Controller NotificationPreferencesRequest $request, TenantNotificationPreferences $preferencesService ): JsonResponse { - $tenant = $request->tenant; + $tenant = $this->resolveTenant($request); $payload = $request->validated()['preferences']; $tenant->update([ @@ -92,7 +93,7 @@ class SettingsController extends Controller */ public function update(SettingsStoreRequest $request): JsonResponse { - $tenant = $request->tenant; + $tenant = $this->resolveTenant($request); $settings = $request->validated()['settings']; $tenant->update([ @@ -115,7 +116,7 @@ class SettingsController extends Controller */ public function reset(Request $request): JsonResponse { - $tenant = $request->tenant; + $tenant = $this->resolveTenant($request); $defaultSettings = [ 'branding' => [ @@ -150,6 +151,35 @@ class SettingsController extends Controller ]); } + private function resolveTenant(Request $request): Tenant + { + $tenant = $request->attributes->get('tenant'); + + if ($tenant instanceof Tenant) { + return $tenant; + } + + $tenantId = $request->attributes->get('tenant_id') + ?? $request->attributes->get('current_tenant_id') + ?? $request->user()?->tenant_id; + + if ($tenantId) { + $tenant = Tenant::query()->find($tenantId); + if ($tenant) { + $request->attributes->set('tenant', $tenant); + + return $tenant; + } + } + + throw new HttpResponseException(ApiError::response( + 'tenant_context_missing', + 'Tenant context missing', + 'Unable to determine tenant for the current request.', + Response::HTTP_UNAUTHORIZED + )); + } + /** * Validate custom domain availability. */ diff --git a/app/Http/Controllers/Api/Tenant/TenantAdminTokenController.php b/app/Http/Controllers/Api/Tenant/TenantAdminTokenController.php index 8470c51..9088664 100644 --- a/app/Http/Controllers/Api/Tenant/TenantAdminTokenController.php +++ b/app/Http/Controllers/Api/Tenant/TenantAdminTokenController.php @@ -8,6 +8,7 @@ use App\Models\Tenant; use App\Models\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -152,16 +153,13 @@ class TenantAdminTokenController extends Controller ]); } - public function exchange(Request $request): JsonResponse + public function exchange(Request $request): JsonResponse|Response { /** @var User|null $user */ $user = Auth::guard('web')->user(); if (! $user) { - return response()->json([ - 'error' => 'unauthenticated', - 'message' => trans('auth.failed'), - ], 401); + return response()->noContent(); } if (! in_array($user->role, ['tenant_admin', 'super_admin'], true)) { diff --git a/resources/js/admin/auth/context.tsx b/resources/js/admin/auth/context.tsx index 3c68580..14ffeb5 100644 --- a/resources/js/admin/auth/context.tsx +++ b/resources/js/admin/auth/context.tsx @@ -51,6 +51,10 @@ async function exchangeSessionForToken(): Promise<{ token: string; abilities: st credentials: 'same-origin', }); + if (response.status === 204) { + return null; + } + if (!response.ok) { return null; } diff --git a/resources/js/admin/components/DevTenantSwitcher.tsx b/resources/js/admin/components/DevTenantSwitcher.tsx index 7ef9622..e6d6d40 100644 --- a/resources/js/admin/components/DevTenantSwitcher.tsx +++ b/resources/js/admin/components/DevTenantSwitcher.tsx @@ -92,7 +92,7 @@ export function DevTenantSwitcher() {

- Select a seeded tenant to mint OAuth tokens and jump straight into their admin space. Available only in development builds. + Select a seeded tenant to mint Sanctum PATs and jump straight into their admin space. Available only in development builds.

{DEV_TENANT_KEYS.map(({ key, label }) => ( diff --git a/resources/js/admin/i18n/locales/de/auth.json b/resources/js/admin/i18n/locales/de/auth.json index 036b3a5..6bc266a 100644 --- a/resources/js/admin/i18n/locales/de/auth.json +++ b/resources/js/admin/i18n/locales/de/auth.json @@ -10,11 +10,11 @@ "Erstelle Einladungen mit personalisierten QR-Codes und teile sie sofort.", "Steuere Aufgaben, Emotionen und Slideshows direkt vom Event aus." ], - "lead": "Du meldest dich über unseren sicheren OAuth-Login an und landest direkt im Event-Dashboard.", + "lead": "Du meldest dich über unser gesichertes Fotospiel-Login an und landest direkt im Event-Dashboard.", "panel_title": "Melde dich an", - "panel_copy": "Logge dich mit deinem Fotospiel-Adminzugang ein. Wir schützen dein Konto mit OAuth 2.1 und klaren Rollenrechten.", + "panel_copy": "Logge dich mit deinem Fotospiel-Adminzugang ein. Wir schützen dein Konto mit persönlichen Zugriffstokens und klaren Rollenrechten.", "actions_title": "Wähle deine Anmeldemethode", - "actions_copy": "Greife sicher per OAuth oder mit deinem Google-Konto auf das Tenant-Dashboard zu.", + "actions_copy": "Greife sicher mit deinem Fotospiel-Login oder deinem Google-Konto auf das Tenant-Dashboard zu.", "cta": "Mit Fotospiel-Login fortfahren", "google_cta": "Mit Google anmelden", "open_account_login": "Konto-Login öffnen", diff --git a/resources/js/admin/i18n/locales/en/auth.json b/resources/js/admin/i18n/locales/en/auth.json index 1a5dad3..def7bc5 100644 --- a/resources/js/admin/i18n/locales/en/auth.json +++ b/resources/js/admin/i18n/locales/en/auth.json @@ -10,11 +10,11 @@ "Create invites with personalized QR codes and share them instantly.", "Run tasks, emotions, and slideshows right from the event dashboard." ], - "lead": "Use our secure OAuth login and land directly in the event dashboard.", + "lead": "Use our secure Fotospiel login and land directly in the event dashboard.", "panel_title": "Sign in", - "panel_copy": "Sign in with your Fotospiel admin access. OAuth 2.1 and clear role permissions keep your account protected.", + "panel_copy": "Sign in with your Fotospiel admin access. Sanctum personal access tokens and clear role permissions keep your account protected.", "actions_title": "Choose your sign-in method", - "actions_copy": "Access the tenant dashboard securely with OAuth or your Google account.", + "actions_copy": "Access the tenant dashboard securely with your Fotospiel login or your Google account.", "cta": "Continue with Fotospiel login", "google_cta": "Continue with Google", "open_account_login": "Open account login", diff --git a/resources/js/admin/pages/LoginPage.tsx b/resources/js/admin/pages/LoginPage.tsx index 0bc4075..8fd15be 100644 --- a/resources/js/admin/pages/LoginPage.tsx +++ b/resources/js/admin/pages/LoginPage.tsx @@ -86,6 +86,8 @@ export default function LoginPage(): JSX.Element { }, }); + const isSubmitting = (mutation as { isPending?: boolean; isLoading: boolean }).isPending ?? mutation.isLoading; + const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); setError(null); @@ -164,11 +166,21 @@ export default function LoginPage(): JSX.Element { diff --git a/tests/Feature/Auth/TenantProfileApiTest.php b/tests/Feature/Auth/TenantProfileApiTest.php index f24947e..1906c69 100644 --- a/tests/Feature/Auth/TenantProfileApiTest.php +++ b/tests/Feature/Auth/TenantProfileApiTest.php @@ -95,4 +95,31 @@ class TenantProfileApiTest extends TestCase $response->assertStatus(401); } + + public function test_exchange_returns_no_content_when_session_missing(): void + { + $response = $this->postJson('/api/v1/tenant-auth/exchange'); + + $response->assertNoContent(); + } + + public function test_exchange_returns_token_for_authenticated_session(): void + { + $tenant = Tenant::factory()->create(); + $user = User::factory()->create([ + 'tenant_id' => $tenant->id, + 'role' => 'tenant_admin', + 'email_verified_at' => now(), + ]); + + $response = $this->actingAs($user)->postJson('/api/v1/tenant-auth/exchange'); + + $response->assertOk(); + $response->assertJsonStructure([ + 'token', + 'token_type', + 'abilities', + 'user' => ['id', 'email', 'role', 'tenant_id'], + ]); + } } diff --git a/tests/Feature/Tenant/Settings/NotificationPreferencesTest.php b/tests/Feature/Tenant/Settings/NotificationPreferencesTest.php new file mode 100644 index 0000000..20c96bd --- /dev/null +++ b/tests/Feature/Tenant/Settings/NotificationPreferencesTest.php @@ -0,0 +1,64 @@ +create([ + 'notification_preferences' => [ + 'photo_thresholds' => false, + ], + ]); + + $user = User::factory()->create([ + 'tenant_id' => $tenant->id, + 'role' => 'tenant_admin', + 'email_verified_at' => now(), + ]); + + Sanctum::actingAs($user, abilities: ['tenant-admin', 'tenant:'.$tenant->id]); + + $response = $this->getJson('/api/v1/tenant/settings/notifications'); + + $response->assertOk(); + $response->assertJsonPath('data.preferences.photo_thresholds', false); + $response->assertJsonStructure([ + 'data' => [ + 'defaults', + 'preferences', + 'overrides', + 'meta' => ['credit_warning_sent_at', 'credit_warning_threshold'], + ], + ]); + } + + public function test_super_admin_can_fetch_notification_preferences_for_specific_tenant(): void + { + $tenant = Tenant::factory()->create(); + + $superAdmin = User::factory()->create([ + 'role' => 'super_admin', + 'tenant_id' => null, + 'email_verified_at' => now(), + ]); + + Sanctum::actingAs($superAdmin, abilities: ['tenant-admin', 'super-admin']); + + $response = $this + ->withHeader('X-Tenant-ID', (string) $tenant->id) + ->getJson('/api/v1/tenant/settings/notifications'); + + $response->assertOk(); + $response->assertJsonPath('data.defaults.photo_thresholds', true); + } +}