fixes login page in tenant admin pwa
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function DevTenantSwitcher() {
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-amber-700">
|
||||
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.
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{DEV_TENANT_KEYS.map(({ key, label }) => (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
@@ -164,11 +166,21 @@ export default function LoginPage(): JSX.Element {
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="mt-2 h-12 w-full justify-center rounded-xl bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-base font-semibold shadow-[0_18px_35px_-18px_rgba(236,72,153,0.7)] transition hover:from-[#ff4470] hover:via-[#ec4899] hover:to-[#4f46e5]"
|
||||
disabled={mutation.isLoading}
|
||||
className={`mt-2 h-12 w-full justify-center rounded-xl bg-gradient-to-r from-[#ff5f87] via-[#ec4899] to-[#6366f1] text-base font-semibold shadow-[0_18px_35px_-18px_rgba(236,72,153,0.7)] transition hover:from-[#ff4470] hover:via-[#ec4899] hover:to-[#4f46e5] ${
|
||||
isSubmitting ? 'cursor-wait opacity-90 saturate-75 shadow-none' : ''
|
||||
}`}
|
||||
disabled={isSubmitting}
|
||||
aria-live="polite"
|
||||
aria-busy={isSubmitting}
|
||||
data-loading={isSubmitting ? 'true' : undefined}
|
||||
>
|
||||
{mutation.isLoading ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : null}
|
||||
{mutation.isLoading ? t('login.loading', 'Anmeldung …') : t('login.submit', 'Anmelden')}
|
||||
<span className="flex w-full items-center justify-center gap-2">
|
||||
<Loader2
|
||||
className={`h-5 w-5 text-white transition-opacity ${isSubmitting ? 'animate-spin opacity-100' : 'opacity-0'}`}
|
||||
aria-hidden={!isSubmitting}
|
||||
/>
|
||||
<span>{isSubmitting ? t('login.loading', 'Anmeldung …') : t('login.submit', 'Anmelden')}</span>
|
||||
</span>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Tenant\Settings;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class NotificationPreferencesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_tenant_admin_can_fetch_notification_preferences(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user