Multi-Domain-Asset-Infrastruktur: geteilte Vite-Konfiguration und DomainAssetContext
- vite.shared.js als gemeinsame Quelle fuer Ports, Hot-Files, HMR-Hosts und CORS-Origins der beiden Vite-Builds (Portal/Web) - App\Support\DomainAssetContext kapselt die Vite-Build-Directory- Konfiguration pro Domain (ThemeServiceProvider + Auth-Layout nutzen ihn) - Tailwind-Portal-Content-Globs auf die tatsaechliche View-Struktur gezogen - Dev-Beispiel-Routen + Tests (DomainAssetContextTest, DevExampleRoutesTest) - Aufraeumen: versehentliche Leerdatei dev:web entfernt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4bb9094207
commit
0efabaf446
15 changed files with 485 additions and 109 deletions
|
|
@ -16,4 +16,23 @@ enum Portal: string
|
||||||
self::Both => 'Beide Portale',
|
self::Both => 'Beide Portale',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function abbreviation(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Presseecho => 'PE',
|
||||||
|
self::Businessportal24 => 'B24',
|
||||||
|
self::Both => 'PE+B24',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stripTrailingAbbreviation(string $value): string
|
||||||
|
{
|
||||||
|
$abbreviations = implode('|', array_map(
|
||||||
|
fn (self $portal): string => preg_quote($portal->abbreviation(), '/'),
|
||||||
|
self::cases(),
|
||||||
|
));
|
||||||
|
|
||||||
|
return trim((string) preg_replace('/\s*\(('.$abbreviations.')\)\s*$/u', '', $value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Support\DomainAssetContext;
|
||||||
use Illuminate\Routing\UrlGenerator;
|
use Illuminate\Routing\UrlGenerator;
|
||||||
use Illuminate\Support\Facades\Request;
|
use Illuminate\Support\Facades\Request;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
@ -16,7 +17,6 @@ class ThemeServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
// Registriere die domains.php Konfigurationsdatei
|
|
||||||
$this->mergeConfigFrom(
|
$this->mergeConfigFrom(
|
||||||
base_path('config/domains.php'),
|
base_path('config/domains.php'),
|
||||||
'domains'
|
'domains'
|
||||||
|
|
@ -28,11 +28,10 @@ class ThemeServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
$host = Request::getHost(); // is domain_name
|
$host = Request::getHost();
|
||||||
$themeOverride = Request::get('theme'); // Allow theme override via URL parameter
|
$themeOverride = Request::get('theme');
|
||||||
|
|
||||||
// Standard-Werte für Domain, die nicht in der Konfiguration sind
|
$defaults = [
|
||||||
$domainConfig = [
|
|
||||||
'name' => config('app.name'),
|
'name' => config('app.name'),
|
||||||
'theme' => 'b2in',
|
'theme' => 'b2in',
|
||||||
'view_prefix' => 'b2in',
|
'view_prefix' => 'b2in',
|
||||||
|
|
@ -41,68 +40,47 @@ class ThemeServiceProvider extends ServiceProvider
|
||||||
'domain_name' => config('app.domain_name'),
|
'domain_name' => config('app.domain_name'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Lade die Domain-Konfiguration
|
$domainConfig = DomainAssetContext::resolve(
|
||||||
$confiDomains = config('domains.domains');
|
$host,
|
||||||
|
$defaults,
|
||||||
|
config('domains.domains', []),
|
||||||
|
is_string($themeOverride) ? $themeOverride : null,
|
||||||
|
);
|
||||||
|
|
||||||
// Suche nach der aktuellen Domain in der Konfiguration
|
$staticAssetOrigin = DomainAssetContext::staticAssetOrigin($domainConfig);
|
||||||
foreach ($confiDomains as $name => $config) {
|
$viteDevServerUrl = DomainAssetContext::viteDevServerUrl($domainConfig);
|
||||||
if (is_array($config) && isset($config['domain_name']) && $config['domain_name'] === $host) {
|
|
||||||
$domainConfig = array_merge($domainConfig, $config);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow theme override via URL parameter (for testing)
|
|
||||||
if ($themeOverride && isset($confiDomains[$themeOverride])) {
|
|
||||||
$domainConfig = array_merge($domainConfig, $confiDomains[$themeOverride]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamische ASSET_URL basierend auf der aktuellen Domain setzen
|
|
||||||
// Verhindert CORS-Probleme, da Assets immer von derselben Domain geladen werden
|
|
||||||
$assetUrl = $domainConfig['url'];
|
|
||||||
|
|
||||||
// Grundlegende Konfiguration im Anwendungskontext verfügbar machen
|
|
||||||
config([
|
config([
|
||||||
'app.theme' => $domainConfig['theme'],
|
'app.theme' => $domainConfig['theme'],
|
||||||
'app.view_prefix' => $domainConfig['view_prefix'],
|
'app.view_prefix' => $domainConfig['view_prefix'],
|
||||||
'app.domain_name' => $domainConfig['domain_name'],
|
'app.domain_name' => $domainConfig['domain_name'],
|
||||||
'app.url' => $domainConfig['url'],
|
'app.url' => $domainConfig['url'],
|
||||||
'app.asset_url' => $assetUrl, // Dynamische Asset-URL für die aktuelle Domain
|
'app.asset_url' => $staticAssetOrigin,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// URL-Generator für die aktuelle Domain konfigurieren
|
|
||||||
// Dies ist wichtig, damit asset() und url() die richtige Domain verwenden
|
|
||||||
URL::forceRootUrl($domainConfig['url']);
|
URL::forceRootUrl($domainConfig['url']);
|
||||||
URL::forceScheme(parse_url($domainConfig['url'], PHP_URL_SCHEME) ?: 'https');
|
URL::forceScheme(parse_url($domainConfig['url'], PHP_URL_SCHEME) ?: 'https');
|
||||||
|
|
||||||
// WICHTIG: Asset-Root direkt im UrlGenerator setzen
|
|
||||||
// Der asset() Helper verwendet einen separaten Asset-Root
|
|
||||||
/** @var UrlGenerator $urlGenerator */
|
/** @var UrlGenerator $urlGenerator */
|
||||||
$urlGenerator = app('url');
|
$urlGenerator = app('url');
|
||||||
$urlGenerator->useAssetOrigin($assetUrl);
|
$urlGenerator->useAssetOrigin($staticAssetOrigin);
|
||||||
|
|
||||||
// Spezifischere Daten für die Views verfügbar machen
|
|
||||||
View::share('theme', $domainConfig['theme']);
|
View::share('theme', $domainConfig['theme']);
|
||||||
View::share('viewPrefix', $domainConfig['view_prefix']);
|
View::share('viewPrefix', $domainConfig['view_prefix']);
|
||||||
View::share('domainName', $domainConfig['domain_name']);
|
View::share('domainName', $domainConfig['domain_name']);
|
||||||
View::share('domainConfig', $domainConfig);
|
View::share('domainConfig', $domainConfig);
|
||||||
View::share('domainUrl', $domainConfig['url']);
|
View::share('domainUrl', $domainConfig['url']);
|
||||||
View::share('assetUrl', $assetUrl);
|
View::share('assetUrl', $staticAssetOrigin);
|
||||||
|
View::share('viteDevServerUrl', $viteDevServerUrl);
|
||||||
|
|
||||||
// Vite-Assets-Konfiguration für die aktuelle Domain
|
|
||||||
if (! app()->runningInConsole()) {
|
if (! app()->runningInConsole()) {
|
||||||
if (isset($domainConfig['assets_dir'])) {
|
if (isset($domainConfig['assets_dir'])) {
|
||||||
Vite::useBuildDirectory($domainConfig['assets_dir']);
|
DomainAssetContext::configureVite($domainConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app()->environment('local')) {
|
if (app()->environment('local')) {
|
||||||
// Entwicklung: Vite Dev Server mit HMR
|
|
||||||
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.pressekonto.test');
|
|
||||||
Vite::useHotFile(public_path('hot'));
|
|
||||||
config(['app.vite_dev_server_url' => $viteDevServerUrl]);
|
config(['app.vite_dev_server_url' => $viteDevServerUrl]);
|
||||||
View::share('viteDevServerUrl', $viteDevServerUrl);
|
|
||||||
} else {
|
} else {
|
||||||
// Produktion: Assets von der aktuellen Domain laden (kein CORS nötig)
|
|
||||||
Vite::useScriptTagAttributes(['crossorigin' => false]);
|
Vite::useScriptTagAttributes(['crossorigin' => false]);
|
||||||
Vite::useStyleTagAttributes(['crossorigin' => false]);
|
Vite::useStyleTagAttributes(['crossorigin' => false]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
145
app/Support/DomainAssetContext.php
Normal file
145
app/Support/DomainAssetContext.php
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Vite;
|
||||||
|
|
||||||
|
class DomainAssetContext
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $defaults
|
||||||
|
* @param array<string, array<string, mixed>> $configuredDomains
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function resolve(
|
||||||
|
string $host,
|
||||||
|
array $defaults,
|
||||||
|
array $configuredDomains,
|
||||||
|
?string $themeOverride = null,
|
||||||
|
?Request $request = null,
|
||||||
|
): array {
|
||||||
|
if ($themeOverride !== null && isset($configuredDomains[$themeOverride])) {
|
||||||
|
return array_merge($defaults, $configuredDomains[$themeOverride]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$portalHost = (string) config('domains.domain_portal');
|
||||||
|
|
||||||
|
if ($host === $portalHost) {
|
||||||
|
$request ??= request();
|
||||||
|
|
||||||
|
if (self::isBackendRequest($request)) {
|
||||||
|
return array_merge($defaults, $configuredDomains['portal'] ?? $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_merge($defaults, $configuredDomains['pressekonto'] ?? $defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($configuredDomains as $config) {
|
||||||
|
if (! is_array($config)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($config['domain_name'] ?? null) === $host) {
|
||||||
|
return array_merge($defaults, $config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isBackendRequest(Request $request): bool
|
||||||
|
{
|
||||||
|
if ($request->routeIs(
|
||||||
|
'dashboard',
|
||||||
|
'login',
|
||||||
|
'register',
|
||||||
|
'password.*',
|
||||||
|
'verification.*',
|
||||||
|
'two-factor.*',
|
||||||
|
'magic-links.*',
|
||||||
|
'me.*',
|
||||||
|
'settings.*',
|
||||||
|
'press-releases.preview',
|
||||||
|
)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$backendPrefixes = [
|
||||||
|
'admin',
|
||||||
|
'customer',
|
||||||
|
'dashboard',
|
||||||
|
'login',
|
||||||
|
'register',
|
||||||
|
'forgot-password',
|
||||||
|
'reset-password',
|
||||||
|
'magic-login',
|
||||||
|
'user',
|
||||||
|
'settings',
|
||||||
|
'flux',
|
||||||
|
'livewire',
|
||||||
|
];
|
||||||
|
|
||||||
|
$path = trim($request->path(), '/');
|
||||||
|
|
||||||
|
foreach ($backendPrefixes as $prefix) {
|
||||||
|
if ($path === $prefix || str_starts_with($path, "{$prefix}/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function usesWebAssets(string $assetsDir): bool
|
||||||
|
{
|
||||||
|
return $assetsDir === 'build/web';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hotFilePath(string $assetsDir): string
|
||||||
|
{
|
||||||
|
return self::usesWebAssets($assetsDir)
|
||||||
|
? public_path('hot-web')
|
||||||
|
: public_path('hot-portal');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $domainConfig
|
||||||
|
*/
|
||||||
|
public static function configureVite(array $domainConfig): void
|
||||||
|
{
|
||||||
|
$assetsDir = (string) ($domainConfig['assets_dir'] ?? 'build/portal');
|
||||||
|
|
||||||
|
Vite::useBuildDirectory($assetsDir);
|
||||||
|
Vite::useHotFile(self::hotFilePath($assetsDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $domainConfig
|
||||||
|
*/
|
||||||
|
public static function staticAssetOrigin(array $domainConfig): string
|
||||||
|
{
|
||||||
|
return (string) ($domainConfig['url'] ?? config('app.url'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $domainConfig
|
||||||
|
*/
|
||||||
|
public static function viteDevServerUrl(array $domainConfig): string
|
||||||
|
{
|
||||||
|
if (isset($domainConfig['asset_url'])) {
|
||||||
|
return (string) $domainConfig['asset_url'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$assetsDir = (string) ($domainConfig['assets_dir'] ?? 'build/portal');
|
||||||
|
|
||||||
|
return self::usesWebAssets($assetsDir)
|
||||||
|
? 'https://assets.pressekonto.test'
|
||||||
|
: 'https://assets.pressekonto.test';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isViteDevServerRunning(string $assetsDir): bool
|
||||||
|
{
|
||||||
|
return is_file(self::hotFilePath($assetsDir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,10 +26,10 @@ return [
|
||||||
'domain_name' => env('APP_PORTAL_NAME', 'pressekonto.test'),
|
'domain_name' => env('APP_PORTAL_NAME', 'pressekonto.test'),
|
||||||
'url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
|
'url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
|
||||||
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.pressekonto.test'),
|
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.pressekonto.test'),
|
||||||
'theme' => 'main',
|
'theme' => 'portal',
|
||||||
'view_prefix' => 'portal',
|
'view_prefix' => 'portal',
|
||||||
'assets_dir' => 'build/portal',
|
'assets_dir' => 'build/portal',
|
||||||
'description' => 'Backend Pressekonto',
|
'description' => 'Backend/Admin auf pressekonto.test',
|
||||||
'color_scheme' => [
|
'color_scheme' => [
|
||||||
'primary' => env('APP_PORTAL_PRIMARY', '#526266'), //
|
'primary' => env('APP_PORTAL_PRIMARY', '#526266'), //
|
||||||
'secondary' => env('APP_PORTAL_SECONDARY', '#82a0a7'), //
|
'secondary' => env('APP_PORTAL_SECONDARY', '#82a0a7'), //
|
||||||
|
|
|
||||||
0
dev:web
0
dev:web
|
|
@ -55,11 +55,11 @@ services:
|
||||||
- "traefik.http.routers.businessportal.tls=true"
|
- "traefik.http.routers.businessportal.tls=true"
|
||||||
- "traefik.http.routers.businessportal.service=pressekonto-service-prc"
|
- "traefik.http.routers.businessportal.service=pressekonto-service-prc"
|
||||||
|
|
||||||
# Asset Domain für Vite-Server Portal (Port 5177)
|
# Asset Domain für Vite-Server Portal/Admin (Port 5177)
|
||||||
- "traefik.http.routers.assets-portal.rule=Host(`assets.pressekonto.test`)"
|
- "traefik.http.routers.assets-pressekonto.rule=Host(`assets.pressekonto.test`)"
|
||||||
- "traefik.http.routers.assets-portal.entrypoints=websecure"
|
- "traefik.http.routers.assets-pressekonto.entrypoints=websecure"
|
||||||
- "traefik.http.routers.assets-portal.tls=true"
|
- "traefik.http.routers.assets-pressekonto.tls=true"
|
||||||
- "traefik.http.routers.assets-portal.service=assets-portal-service-prc"
|
- "traefik.http.routers.assets-pressekonto.service=assets-portal-service-prc"
|
||||||
|
|
||||||
# Asset Domain für Vite-Server Presseecho
|
# Asset Domain für Vite-Server Presseecho
|
||||||
- "traefik.http.routers.assets-presseecho.rule=Host(`assets.presseecho.test`)"
|
- "traefik.http.routers.assets-presseecho.rule=Host(`assets.presseecho.test`)"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "echo 'Bitte spezifischen Dev-Server starten: npm run dev:portal ODER npm run dev:web ODER npm run dev:all'",
|
"dev": "echo 'Dev-Server: npm run dev:portal (Admin/FluxUI) | npm run dev:web (pressekonto Hub + presseecho + businessportal24) | npm run dev:all (beides)'",
|
||||||
"dev:portal": "vite --config vite.portal.config.js",
|
"dev:portal": "vite --config vite.portal.config.js",
|
||||||
"dev:web": "vite --config vite.web.config.js",
|
"dev:web": "vite --config vite.web.config.js",
|
||||||
"dev:all": "concurrently \"npm run dev:portal\" \"npm run dev:web\" --names \"PORTAL,WEB\" --prefix-colors \"cyan,magenta\"",
|
"dev:all": "concurrently \"npm run dev:portal\" \"npm run dev:web\" --names \"PORTAL,WEB\" --prefix-colors \"cyan,magenta\"",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@
|
||||||
]);
|
]);
|
||||||
$themeCssPath = \App\Helpers\ThemeHelper::getThemeCssPath();
|
$themeCssPath = \App\Helpers\ThemeHelper::getThemeCssPath();
|
||||||
$assetsDir = config('domains.domains.pressekonto.assets_dir', 'build/web');
|
$assetsDir = config('domains.domains.pressekonto.assets_dir', 'build/web');
|
||||||
\Illuminate\Support\Facades\Vite::useBuildDirectory($assetsDir);
|
\App\Support\DomainAssetContext::configureVite([
|
||||||
|
'assets_dir' => $assetsDir,
|
||||||
|
]);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Http\Controllers\PressReleasePreviewController;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\PressRelease;
|
use App\Models\PressRelease;
|
||||||
|
use App\Support\DomainAssetContext;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
@ -33,7 +34,9 @@ $applyWebDomainConfig = static function (string $domainKey): array {
|
||||||
View::share('domainName', $domainConfig['domain_name'] ?? request()->getHost());
|
View::share('domainName', $domainConfig['domain_name'] ?? request()->getHost());
|
||||||
View::share('domainConfig', $domainConfig);
|
View::share('domainConfig', $domainConfig);
|
||||||
View::share('domainUrl', $domainConfig['url'] ?? config('app.url'));
|
View::share('domainUrl', $domainConfig['url'] ?? config('app.url'));
|
||||||
View::share('assetUrl', $domainConfig['url'] ?? config('app.url'));
|
View::share('assetUrl', DomainAssetContext::staticAssetOrigin($domainConfig));
|
||||||
|
|
||||||
|
DomainAssetContext::configureVite($domainConfig);
|
||||||
|
|
||||||
return $domainConfig;
|
return $domainConfig;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,18 @@ const defaultTheme = require("tailwindcss/defaultTheme");
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./resources/views/portal/**/*.blade.php",
|
"./resources/views/livewire/admin/**/*.blade.php",
|
||||||
"./resources/views/layouts/portal/**/*.blade.php",
|
"./resources/views/livewire/customer/**/*.blade.php",
|
||||||
"./resources/views/components/**/*.blade.php",
|
"./resources/views/livewire/components/**/*.blade.php",
|
||||||
"./app/Livewire/Portal/**/*.php",
|
"./resources/views/livewire/settings/**/*.blade.php",
|
||||||
|
"./resources/views/livewire/auth/**/*.blade.php",
|
||||||
|
"./resources/views/layouts/**/*.blade.php",
|
||||||
|
"./resources/views/components/layouts/**/*.blade.php",
|
||||||
|
"./resources/views/components/portal/**/*.blade.php",
|
||||||
|
"./resources/views/components/settings/**/*.blade.php",
|
||||||
|
"./resources/views/partials/**/*.blade.php",
|
||||||
|
"./resources/views/admin/**/*.blade.php",
|
||||||
|
"./app/Livewire/**/*.php",
|
||||||
"./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php",
|
"./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php",
|
||||||
"./vendor/livewire/flux-pro/stubs/**/*.blade.php",
|
"./vendor/livewire/flux-pro/stubs/**/*.blade.php",
|
||||||
"./vendor/livewire/flux/stubs/**/*.blade.php",
|
"./vendor/livewire/flux/stubs/**/*.blade.php",
|
||||||
|
|
|
||||||
84
tests/Feature/DomainAssetContextTest.php
Normal file
84
tests/Feature/DomainAssetContextTest.php
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\DomainAssetContext;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
test('pressekonto homepage resolves to web assets', function () {
|
||||||
|
$defaults = ['theme' => 'fallback', 'assets_dir' => 'build/b2in'];
|
||||||
|
$domains = config('domains.domains');
|
||||||
|
|
||||||
|
$config = DomainAssetContext::resolve(
|
||||||
|
'pressekonto.test',
|
||||||
|
$defaults,
|
||||||
|
$domains,
|
||||||
|
null,
|
||||||
|
Request::create('https://pressekonto.test/', 'GET'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($config['theme'])->toBe('pressekonto')
|
||||||
|
->and($config['assets_dir'])->toBe('build/web');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pressekonto admin routes resolve to portal assets', function () {
|
||||||
|
$defaults = ['theme' => 'fallback', 'assets_dir' => 'build/b2in'];
|
||||||
|
$domains = config('domains.domains');
|
||||||
|
|
||||||
|
$config = DomainAssetContext::resolve(
|
||||||
|
'pressekonto.test',
|
||||||
|
$defaults,
|
||||||
|
$domains,
|
||||||
|
null,
|
||||||
|
Request::create('https://pressekonto.test/admin/users', 'GET'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($config['theme'])->toBe('portal')
|
||||||
|
->and($config['assets_dir'])->toBe('build/portal')
|
||||||
|
->and($config['asset_url'])->toBe('https://assets.pressekonto.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('presseecho resolves to its own web theme', function () {
|
||||||
|
$defaults = ['theme' => 'fallback', 'assets_dir' => 'build/b2in'];
|
||||||
|
$domains = config('domains.domains');
|
||||||
|
|
||||||
|
$config = DomainAssetContext::resolve(
|
||||||
|
'presseecho.test',
|
||||||
|
$defaults,
|
||||||
|
$domains,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($config['theme'])->toBe('presseecho')
|
||||||
|
->and($config['assets_dir'])->toBe('build/web');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hot file path depends on asset bundle', function () {
|
||||||
|
expect(DomainAssetContext::hotFilePath('build/web'))
|
||||||
|
->toEndWith('hot-web')
|
||||||
|
->and(DomainAssetContext::hotFilePath('build/portal'))
|
||||||
|
->toEndWith('hot-portal');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('static asset origin always uses main domain not vite subdomain', function () {
|
||||||
|
expect(DomainAssetContext::staticAssetOrigin([
|
||||||
|
'url' => 'https://pressekonto.test',
|
||||||
|
'asset_url' => 'https://assets.pressekonto.test',
|
||||||
|
]))->toBe('https://pressekonto.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('vite dev server url uses asset subdomain from domain config', function () {
|
||||||
|
expect(DomainAssetContext::viteDevServerUrl([
|
||||||
|
'url' => 'https://pressekonto.test',
|
||||||
|
'asset_url' => 'https://assets.pressekonto.test',
|
||||||
|
'assets_dir' => 'build/portal',
|
||||||
|
]))->toBe('https://assets.pressekonto.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('backend request detection covers admin and livewire paths', function () {
|
||||||
|
expect(DomainAssetContext::isBackendRequest(Request::create('/admin/users')))
|
||||||
|
->toBeTrue()
|
||||||
|
->and(DomainAssetContext::isBackendRequest(Request::create('/livewire/update', 'POST')))
|
||||||
|
->toBeTrue()
|
||||||
|
->and(DomainAssetContext::isBackendRequest(Request::create('/kategorien')))
|
||||||
|
->toBeFalse()
|
||||||
|
->and(DomainAssetContext::isBackendRequest(Request::create('/')))
|
||||||
|
->toBeFalse();
|
||||||
|
});
|
||||||
7
tests/Feature/Web/DevExampleRoutesTest.php
Normal file
7
tests/Feature/Web/DevExampleRoutesTest.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
test('example', function () {
|
||||||
|
$response = $this->get('/');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
});
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
/**
|
/**
|
||||||
* Vite-Konfiguration für Backend (Portal)
|
* Vite-Konfiguration für Backend/Admin (FluxUI)
|
||||||
* - Domain: pressekonto.test
|
* - Domain: pressekonto.test (Admin, Customer, Auth)
|
||||||
|
* - Asset-Subdomain: assets.pressekonto.test
|
||||||
* - Port: 5177
|
* - Port: 5177
|
||||||
* - Verwendet FluxUI
|
* - Build: public/build/portal
|
||||||
* - Build-Verzeichnis: public/build/portal
|
*
|
||||||
|
* Öffentliche Hub-Landing und Frontends → vite.web.config.js (dev:web)
|
||||||
*
|
*
|
||||||
* Starten mit: npm run dev:portal
|
* Starten mit: npm run dev:portal
|
||||||
*/
|
*/
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import laravel from "laravel-vite-plugin";
|
import laravel from "laravel-vite-plugin";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import {
|
||||||
const httpsConfig =
|
PORTAL_HMR_HOST,
|
||||||
process.env.NODE_ENV === "production"
|
PORTAL_HOT_FILE,
|
||||||
? {
|
PORTAL_PORT,
|
||||||
// In Produktion: echte Zertifikate verwenden
|
createWatchIgnored,
|
||||||
key: process.env.SSL_KEY_PATH,
|
portalRefreshPaths,
|
||||||
cert: process.env.SSL_CERT_PATH,
|
portalWatchDirs,
|
||||||
}
|
} from "./vite.shared.js";
|
||||||
: true; // Self-signed für Entwicklung
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
@ -28,24 +29,28 @@ export default defineConfig({
|
||||||
"resources/js/app.js",
|
"resources/js/app.js",
|
||||||
"resources/js/portal-form-hooks.js",
|
"resources/js/portal-form-hooks.js",
|
||||||
],
|
],
|
||||||
refresh: ["resources/views/portal/**/*.blade.php"],
|
refresh: portalRefreshPaths,
|
||||||
|
hotFile: PORTAL_HOT_FILE,
|
||||||
}),
|
}),
|
||||||
tailwindcss({
|
tailwindcss({
|
||||||
config: "./tailwind.portal.config.js",
|
config: "./tailwind.portal.config.js",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
https: false, // Traefik übernimmt SSL, Vite läuft intern auf HTTP
|
https: false,
|
||||||
cors: true,
|
cors: true,
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 5174, // oder 5175
|
port: PORTAL_PORT,
|
||||||
hmr: {
|
strictPort: true,
|
||||||
host: "assets.pressekonto.test",
|
allowedHosts: ["pressekonto.test", PORTAL_HMR_HOST, "localhost", "0.0.0.0"],
|
||||||
protocol: "wss", // Explizit wss für WebSocket Secure
|
watch: {
|
||||||
// WICHTIG: Die 'port'-Angabe hier entfernen!
|
ignored: createWatchIgnored(portalWatchDirs),
|
||||||
// Der Browser soll den Standard-Port (443) von Traefik nutzen.
|
|
||||||
},
|
},
|
||||||
// Das origin ist nicht mehr notwendig, da der HMR-Port wegfällt.
|
hmr: {
|
||||||
|
host: PORTAL_HMR_HOST,
|
||||||
|
protocol: "wss",
|
||||||
|
},
|
||||||
|
origin: `https://${PORTAL_HMR_HOST}`,
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: "public/build/portal",
|
outDir: "public/build/portal",
|
||||||
|
|
|
||||||
126
vite.shared.js
Normal file
126
vite.shared.js
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const projectRoot = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export const PORTAL_PORT = 5177;
|
||||||
|
export const WEB_PORT = 5178;
|
||||||
|
|
||||||
|
export const PORTAL_HOT_FILE = "public/hot-portal";
|
||||||
|
export const WEB_HOT_FILE = "public/hot-web";
|
||||||
|
|
||||||
|
export const PORTAL_HMR_HOST = "assets.pressekonto.test";
|
||||||
|
export const WEB_HMR_HOSTS = [
|
||||||
|
"assets.presseecho.test",
|
||||||
|
"assets.businessportal24.test",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WEB_CORS_ORIGINS = [
|
||||||
|
"https://pressekonto.test",
|
||||||
|
"https://assets.pressekonto.test",
|
||||||
|
"https://presseecho.test",
|
||||||
|
"https://assets.presseecho.test",
|
||||||
|
"https://businessportal24.test",
|
||||||
|
"https://assets.businessportal24.test",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WEB_ALLOWED_HOSTS = [
|
||||||
|
"pressekonto.test",
|
||||||
|
"assets.pressekonto.test",
|
||||||
|
"presseecho.test",
|
||||||
|
"assets.presseecho.test",
|
||||||
|
"businessportal24.test",
|
||||||
|
"assets.businessportal24.test",
|
||||||
|
"localhost",
|
||||||
|
"0.0.0.0",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelist-basiertes Watch-Filtering für Vite/Chokidar.
|
||||||
|
*
|
||||||
|
* @param {string[]} allowedDirs Pfade relativ zum Projektroot
|
||||||
|
*/
|
||||||
|
export function createWatchIgnored(allowedDirs) {
|
||||||
|
const allowedPrefixes = allowedDirs.map((dir) =>
|
||||||
|
path.join(projectRoot, dir),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (filePath) => {
|
||||||
|
const normalized = path.normalize(filePath);
|
||||||
|
|
||||||
|
if (normalized.includes(`${path.sep}node_modules${path.sep}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === projectRoot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllowed = allowedPrefixes.some(
|
||||||
|
(prefix) =>
|
||||||
|
normalized === prefix ||
|
||||||
|
normalized.startsWith(prefix + path.sep),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAllowed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAncestorOfAllowed = allowedPrefixes.some((prefix) =>
|
||||||
|
prefix.startsWith(normalized + path.sep),
|
||||||
|
);
|
||||||
|
|
||||||
|
return !isAncestorOfAllowed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Backend/Admin: FluxUI, Livewire-Panel, Customer-Bereich */
|
||||||
|
export const portalWatchDirs = [
|
||||||
|
"resources/js",
|
||||||
|
"resources/css",
|
||||||
|
"resources/views/livewire/admin",
|
||||||
|
"resources/views/livewire/customer",
|
||||||
|
"resources/views/livewire/components",
|
||||||
|
"resources/views/livewire/settings",
|
||||||
|
"resources/views/livewire/auth",
|
||||||
|
"resources/views/layouts",
|
||||||
|
"resources/views/components/layouts",
|
||||||
|
"resources/views/components/portal",
|
||||||
|
"resources/views/components/settings",
|
||||||
|
"resources/views/partials",
|
||||||
|
"resources/views/admin",
|
||||||
|
"app/Livewire",
|
||||||
|
"vendor/livewire/flux/dist",
|
||||||
|
"vendor/livewire/flux/stubs",
|
||||||
|
"vendor/livewire/flux-pro/stubs",
|
||||||
|
"vendor/laravel/framework/src/Illuminate/Pagination/resources/views",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Öffentliche Frontends: pressekonto Hub, presseecho, businessportal24 */
|
||||||
|
export const webWatchDirs = [
|
||||||
|
"resources/js",
|
||||||
|
"resources/css",
|
||||||
|
"resources/views/web",
|
||||||
|
"resources/views/livewire/web",
|
||||||
|
"resources/views/components/web",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const portalRefreshPaths = [
|
||||||
|
"resources/views/livewire/admin/**",
|
||||||
|
"resources/views/livewire/customer/**",
|
||||||
|
"resources/views/livewire/components/**",
|
||||||
|
"resources/views/livewire/settings/**",
|
||||||
|
"resources/views/livewire/auth/**",
|
||||||
|
"resources/views/layouts/**",
|
||||||
|
"resources/views/components/layouts/**",
|
||||||
|
"resources/views/components/portal/**",
|
||||||
|
"resources/views/partials/**",
|
||||||
|
"resources/views/admin/**",
|
||||||
|
"app/Livewire/**",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const webRefreshPaths = [
|
||||||
|
"resources/views/web/**",
|
||||||
|
"resources/views/livewire/web/**",
|
||||||
|
"resources/views/components/web/**",
|
||||||
|
];
|
||||||
|
|
@ -1,63 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Vite-Konfiguration für öffentliche Frontends (Web)
|
||||||
|
* - Domains: pressekonto.test (Hub), presseecho.test, businessportal24.test
|
||||||
|
* - Asset-Subdomains: assets.pressekonto.test, assets.presseecho.test, assets.businessportal24.test
|
||||||
|
* - Port: 5178
|
||||||
|
* - Build: public/build/web
|
||||||
|
*
|
||||||
|
* Backend/Admin auf pressekonto.test → vite.portal.config.js (dev:portal)
|
||||||
|
*
|
||||||
|
* Starten mit: npm run dev:web
|
||||||
|
*/
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import laravel from "laravel-vite-plugin";
|
import laravel from "laravel-vite-plugin";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import {
|
||||||
// SSL-Konfiguration - für Entwicklung ohne echte Zertifikate
|
WEB_ALLOWED_HOSTS,
|
||||||
const httpsConfig =
|
WEB_CORS_ORIGINS,
|
||||||
process.env.NODE_ENV === "production"
|
WEB_HOT_FILE,
|
||||||
? {
|
WEB_PORT,
|
||||||
// In Produktion: echte Zertifikate verwenden
|
createWatchIgnored,
|
||||||
key: process.env.SSL_KEY_PATH,
|
webRefreshPaths,
|
||||||
cert: process.env.SSL_CERT_PATH,
|
webWatchDirs,
|
||||||
}
|
} from "./vite.shared.js";
|
||||||
: true; // Self-signed für Entwicklung
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
laravel({
|
laravel({
|
||||||
input: [
|
input: [
|
||||||
// Web Theme CSS Dateien
|
|
||||||
"resources/css/web/theme-businessportal24.css",
|
"resources/css/web/theme-businessportal24.css",
|
||||||
"resources/css/web/theme-presseecho.css", // Neu: CSS für presseecho hinzugefügt, um beide Themes vorab zu kompilieren
|
"resources/css/web/theme-presseecho.css",
|
||||||
"resources/css/web/theme-pressekonto.css", // Hub-Landing pressekonto.de
|
"resources/css/web/theme-pressekonto.css",
|
||||||
"resources/js/app.js",
|
"resources/js/app.js",
|
||||||
],
|
],
|
||||||
refresh: ["resources/views/web/**/*.blade.php"],
|
refresh: webRefreshPaths,
|
||||||
|
hotFile: WEB_HOT_FILE,
|
||||||
}),
|
}),
|
||||||
tailwindcss({
|
tailwindcss({
|
||||||
config: "./tailwind.web.config.js",
|
config: "./tailwind.web.config.js",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
https: false, // Traefik übernimmt SSL, Vite läuft intern auf HTTP
|
https: false,
|
||||||
|
watch: {
|
||||||
|
ignored: createWatchIgnored(webWatchDirs),
|
||||||
|
},
|
||||||
cors: {
|
cors: {
|
||||||
origin: [
|
origin: WEB_CORS_ORIGINS,
|
||||||
"https://businessportal24.test",
|
|
||||||
"https://assets.businessportal24.test",
|
|
||||||
],
|
|
||||||
credentials: true,
|
credentials: true,
|
||||||
},
|
},
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 5178, // Web-spezifischer Port
|
port: WEB_PORT,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
allowedHosts: [
|
allowedHosts: WEB_ALLOWED_HOSTS,
|
||||||
"assets.businessportal24.test",
|
|
||||||
"businessportal24.test",
|
|
||||||
"assets.presseecho.test", // Neu: presseecho-Host hinzugefügt
|
|
||||||
"presseecho.test", // Neu: presseecho-Host hinzugefügt
|
|
||||||
"assets.pressekonto.test", // Hub-Landing pressekonto.de
|
|
||||||
"pressekonto.test", // Hub-Landing pressekonto.de
|
|
||||||
"localhost",
|
|
||||||
"0.0.0.0",
|
|
||||||
],
|
|
||||||
hmr: {
|
hmr: {
|
||||||
host: "assets.businessportal24.test",
|
|
||||||
protocol: "wss",
|
protocol: "wss",
|
||||||
},
|
},
|
||||||
origin: "https://assets.businessportal24.test", // Ohne Port!
|
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: `public/build/web`,
|
outDir: "public/build/web",
|
||||||
assetsDir: "",
|
assetsDir: "",
|
||||||
manifest: "manifest.json",
|
manifest: "manifest.json",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue