Audit: dev/sicherheit/SICHERHEITSAUDIT-2026-10-07.md, Plan und Entscheidungen: dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md. - S-01 Legacy-Rechnungen fremder Kunden nur mit billing:manage (auch Kundenroute). - S-02 Kontaktzuordnung auf der Firmenanzeige verlangt companies:write, $id gesperrt. - S-03 CompanyMembershipObserver: automatische Firmenrechte werden bei jeder Adress-/Zuordnungsänderung sofort neu bewertet; memberships:resync-auto. - S-04 Passwortwechsel beendet alle anderen Sessions (PasswordChanger), Reset alle Sessions und offene Login-Links; Fortify PUT /user/password abgeschaltet. API-Tokens bleiben bestehen. - S-05 Google: email_verified Pflicht, feste Bindung über die Provider-ID; Erstverknüpfung bestehender Konten außerhalb gmail/hd nur per Bestätigungslink in derselben Browser-Session. - S-06 Frischer Identitätsnachweis (Passwort, App-Code, E-Mail-Code oder Passkey) vor E-Mail-Wechsel, erster 2FA-Einrichtung und Token-Erzeugung; Hinweis-Mails an die (bisherige) Kontoadresse. - Entscheidung 5: Passkeys (Fortify, @laravel/passkeys) für alle; Super-Admins und Admins bestätigen die Session für den Admin-Bereich per Passkey oder App-Code (EnsureStrongAdminAuthentication, Notfallschalter AUTH_REQUIRE_STRONG_ADMIN); Reset des zweiten Faktors durch Super-Admins. Neue Migration: create_passkeys_table. Suite 2.403 grün (1 übersprungen), Larastan ohne Meldung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
7.3 KiB
PHP
172 lines
7.3 KiB
PHP
@props([
|
||
'title' => null,
|
||
'eyebrow' => 'Publisher-Hub',
|
||
'heading' => null,
|
||
'topRightLabel' => null,
|
||
'topRightLinkText' => null,
|
||
'topRightLinkHref' => null,
|
||
'showFromBanner' => true,
|
||
])
|
||
|
||
@php
|
||
$brand = config('domains.domains.pressekonto.brand', []);
|
||
$from = request()->query('from');
|
||
|
||
$brandLabelMap = [
|
||
'presseecho' => 'presseecho.de',
|
||
'businessportal24' => 'businessportal24.com',
|
||
];
|
||
$fromBrandLabel = $brandLabelMap[$from] ?? null;
|
||
|
||
$pageTitle = $title ?? ($brand['meta_title'] ?? 'pressekonto – Publisher-Hub');
|
||
|
||
config([
|
||
'app.theme' => 'pressekonto',
|
||
'app.view_prefix' => 'web',
|
||
]);
|
||
$themeCssPath = \App\Helpers\ThemeHelper::getThemeCssPath();
|
||
$assetsDir = config('domains.domains.pressekonto.assets_dir', 'build/web');
|
||
\App\Support\DomainAssetContext::configureVite([
|
||
'assets_dir' => $assetsDir,
|
||
]);
|
||
@endphp
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||
{{-- Login/Registrierung gehören nicht in den Index (Anhang 2, Abschnitt 7). --}}
|
||
<meta name="robots" content="noindex, nofollow" />
|
||
|
||
{{-- Der Auth-Bereich ist bewusst immer im Hellmodus; Dark/Light gibt es nur
|
||
im eingeloggten Portal. Eine aus dem Portal übernommene .dark-Klasse
|
||
(flux_appearance-Cookie / DOM-Übernahme) wird hier sofort entfernt,
|
||
bevor die Styles greifen – sonst erscheint der Login dunkel. --}}
|
||
<script>document.documentElement.classList.remove('dark');</script>
|
||
|
||
<title>{{ $pageTitle }}</title>
|
||
|
||
<link rel="icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}" type="image/svg+xml" />
|
||
<link rel="apple-touch-icon" href="{{ asset(\App\Helpers\ThemeHelper::getAppIconPath()) }}" />
|
||
|
||
@include('partials.local-fonts')
|
||
|
||
{{-- Nur CSS aus dem Web-Build laden. Alpine bringt @livewireScripts mit;
|
||
würden wir hier zusätzlich resources/js/app.js mit Alpine.start()
|
||
laden, gäbe es zwei Alpine-Instanzen und wire:submit, x-data,
|
||
wire:model würden brechen (siehe Browser-Logs vor diesem Fix). --}}
|
||
@vite([$themeCssPath], $assetsDir)
|
||
{{-- Passkey-Login (Entscheidung 5): kleiner Client ohne eigenes Alpine. --}}
|
||
@vite(['resources/js/passkeys.js'], 'build/portal')
|
||
@livewireStyles
|
||
<x-monitoring.browser-errors :build="$assetsDir" area="auth" />
|
||
</head>
|
||
|
||
<body class="font-sans text-ink antialiased" style="background-color: #f6f4ef;">
|
||
|
||
<div class="relative min-h-screen flex flex-col overflow-hidden bg-bg">
|
||
|
||
{{-- Atmosphäre: subtiles Raster --}}
|
||
<div class="auth-grid" aria-hidden="true"></div>
|
||
|
||
{{-- Atmosphäre: konzentrische Kreise um die Bildmitte --}}
|
||
<svg class="absolute inset-0 w-full h-full pointer-events-none" preserveAspectRatio="xMidYMid slice" viewBox="0 0 1280 880" aria-hidden="true">
|
||
<g opacity="0.09" stroke="#1A2540" fill="none" stroke-width="1">
|
||
<circle cx="640" cy="470" r="160" />
|
||
<circle cx="640" cy="470" r="260" />
|
||
<circle cx="640" cy="470" r="380" />
|
||
<circle cx="640" cy="470" r="510" />
|
||
<circle cx="640" cy="470" r="660" />
|
||
</g>
|
||
</svg>
|
||
|
||
{{-- 3px Hub-Blau-Streifen --}}
|
||
<div class="relative h-[3px] bg-hub z-10"></div>
|
||
|
||
{{-- Header --}}
|
||
<header class="relative z-10 px-6 sm:px-10 py-[22px] flex items-center justify-between gap-4">
|
||
<a href="{{ route('home') }}" class="flex items-baseline gap-2.5 no-underline" wire:navigate>
|
||
<span class="text-[19px] font-bold tracking-[-0.4px] leading-none">
|
||
<x-web.brand-mark brand="pressekonto" :serif="false" />
|
||
</span>
|
||
<span class="hidden sm:inline-block w-px h-[14px] bg-bg-rule"></span>
|
||
<span class="hidden sm:inline-block text-[9.5px] font-bold tracking-[0.22em] uppercase text-ink-3">
|
||
{{ $brand['tagline_short'] ?? 'Publisher · Hub' }}
|
||
</span>
|
||
</a>
|
||
|
||
@if ($topRightLinkText && $topRightLinkHref)
|
||
<span class="text-[13px] text-ink-3">
|
||
@if ($topRightLabel)
|
||
{{ __($topRightLabel) }}
|
||
@endif
|
||
<a href="{{ $topRightLinkHref }}" class="link-hub" wire:navigate>{{ __($topRightLinkText) }}</a>
|
||
</span>
|
||
@endif
|
||
</header>
|
||
|
||
{{-- Auth-Card --}}
|
||
<main class="relative z-10 flex-1 flex items-center justify-center px-6 sm:px-10 py-5">
|
||
<div class="w-full max-w-[440px]">
|
||
|
||
@if ($showFromBanner && $fromBrandLabel)
|
||
<div class="from-banner mb-3.5">
|
||
{{ __('Sie kommen von') }} <strong class="font-semibold text-ink">{{ $fromBrandLabel }}</strong>.
|
||
{{ __('Ihr Konto funktioniert für') }} <strong class="font-semibold text-ink">{{ __('beide Portale') }}</strong> –
|
||
presseecho.de und businessportal24.com.
|
||
</div>
|
||
@elseif ($showFromBanner)
|
||
<div class="from-banner mb-3.5">
|
||
{{ __('Ihr Konto funktioniert auch für') }}
|
||
<strong class="font-semibold text-ink">presseecho.de</strong>
|
||
{{ __('und') }}
|
||
<strong class="font-semibold text-ink">businessportal24.com</strong>.
|
||
</div>
|
||
@endif
|
||
|
||
<div class="auth-card">
|
||
@if ($eyebrow)
|
||
{{-- Heading/Eyebrow kommen als deutsche Strings aus dem #[Layout]-
|
||
Attribut (dort sind keine __()-Aufrufe erlaubt); hier am
|
||
Ausgabepunkt übersetzt, damit sie als Schlüssel wirken. --}}
|
||
<div class="eyebrow-hub mb-2.5">{{ __($eyebrow) }}</div>
|
||
@endif
|
||
|
||
@if ($heading)
|
||
<h1 class="text-[26px] font-bold tracking-[-0.5px] leading-[1.2] m-0 mb-7 text-ink">
|
||
{{ __($heading) }}
|
||
</h1>
|
||
@endif
|
||
|
||
{{ $slot }}
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
{{-- Micro-Footer --}}
|
||
<footer class="relative z-10 px-6 sm:px-10 pt-[18px] pb-[26px] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-[11.5px] text-ink-3">
|
||
<span>
|
||
SSL <span class="text-ink-4 mx-2">·</span>
|
||
Daten in DE <span class="text-ink-4 mx-2">·</span>
|
||
2-Faktor verfügbar
|
||
</span>
|
||
<span class="flex flex-wrap items-center gap-x-1 gap-y-1">
|
||
<a href="{{ route('impressum') }}" class="text-ink-3 no-underline hover:text-ink">Impressum</a>
|
||
<span class="text-ink-4 mx-2">·</span>
|
||
<a href="{{ route('datenschutz') }}" class="text-ink-3 no-underline hover:text-ink">Datenschutz</a>
|
||
<span class="text-ink-4 mx-2">·</span>
|
||
<a href="{{ route('agb') }}" class="text-ink-3 no-underline hover:text-ink">AGB</a>
|
||
<span class="text-ink-4 mx-2">·</span>
|
||
<a href="{{ route('hilfe') }}" class="text-ink-3 no-underline hover:text-ink">Support</a>
|
||
</span>
|
||
</footer>
|
||
</div>
|
||
|
||
{{-- Schwebendes Kontakt-Widget (unten rechts, Auth-Bereich) --}}
|
||
<x-web.contact-launcher />
|
||
|
||
@livewireScripts
|
||
</body>
|
||
</html>
|