23-01-2026

This commit is contained in:
Kevin Adametz 2026-01-23 17:33:10 +01:00
parent 07959c0ba2
commit 854ce02bf6
166 changed files with 32909 additions and 1262 deletions

View file

@ -10,6 +10,15 @@ class ThemeHelper
public static function getLogoPath(string $type = 'positive'): string
{
$theme = config('app.theme', 'b2in');
return self::getLogoPathForBrand($theme, $type);
}
/**
* Get the logo path for a specific brand
*/
public static function getLogoPathForBrand(?string $brand = null, string $type = 'positive'): string
{
$brand = $brand ?? config('app.theme', 'b2in');
$logoMap = [
'b2in' => [
@ -30,7 +39,7 @@ class ThemeHelper
]
];
return $logoMap[$theme][$type] ?? $logoMap['b2in'][$type];
return $logoMap[$brand][$type] ?? $logoMap['b2in'][$type];
}
/**
@ -110,4 +119,45 @@ class ThemeHelper
$config = self::getDomainConfig();
return $config['url'] ?? config('app.url');
}
/**
* Get brand colors for a specific brand
*/
public static function getBrandColors(?string $brand = null): array
{
$brand = $brand ?? config('app.theme', 'b2in');
$config = config("domains.domains.{$brand}", []);
return [
'primary' => $config['color_scheme']['primary'] ?? '#2b3f51',
'secondary' => $config['color_scheme']['secondary'] ?? '#20a0da',
'accent' => $config['color_scheme']['accent'] ?? null,
];
}
/**
* Get brand name for a specific brand
*/
public static function getBrandName(?string $brand = null): string
{
$brand = $brand ?? config('app.theme', 'b2in');
$brandNames = [
'b2in' => 'B2IN',
'b2a' => 'B2A',
'stileigentum' => 'StilEigentum',
'style2own' => 'Style2Own',
];
return $brandNames[$brand] ?? strtoupper($brand);
}
/**
* Get all brand configuration for a specific brand
*/
public static function getBrandConfig(?string $brand = null): array
{
$brand = $brand ?? config('app.theme', 'b2in');
return config("domains.domains.{$brand}", []);
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\DisplayVideo;
use App\Models\DisplayFooterContent;
use Illuminate\Http\JsonResponse;
class DisplayConfigController extends Controller
{
/**
* Gibt die Konfiguration für die Display-Seite zurück
*/
public function index(): JsonResponse
{
$videos = DisplayVideo::active()->get()->map(function ($video) {
return [
'src' => $video->full_path,
'position' => $video->position,
];
});
$footerContent = DisplayFooterContent::active()->get()->map(function ($footer) {
$data = [
'headline' => $footer->headline,
'subline' => $footer->subline,
];
// URL nur hinzufügen wenn vorhanden
if ($footer->url) {
$data['url'] = $footer->short_url;
}
return $data;
});
return response()->json([
'videoPlaylist' => $videos,
'footerContent' => $footerContent,
]);
}
}

View file

@ -28,6 +28,16 @@ class BasicAuthMiddleware
return $next($request);
}
// Skip Basic Auth für Flux UI Assets (flux.js, flux.min.js, editor.js, etc.)
if (str_starts_with($path, 'flux/')) {
return $next($request);
}
// Skip Basic Auth für Display-API und Short-Links (öffentlicher Zugriff für Display-Seite)
if ($request->is('api/display/*') || $request->is('_cabinet/*')) {
return $next($request);
}
// Credentials from .env file
$user = config('auth.basic.user');
$pass = config('auth.basic.password');

View file

@ -0,0 +1,59 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response;
/**
* Middleware um die URL-Konfiguration basierend auf der aktuellen Domain zu setzen.
*
* Diese Middleware muss sehr früh im Request-Lifecycle ausgeführt werden,
* um sicherzustellen, dass url() und asset() die richtige Domain verwenden.
*/
class SetDomainUrl
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
$host = $request->getHost();
// Suche nach der Domain-Konfiguration
$domainConfig = null;
$domains = config('domains.domains', []);
foreach ($domains as $name => $config) {
if (is_array($config) && isset($config['domain_name']) && $config['domain_name'] === $host) {
$domainConfig = $config;
break;
}
}
// Wenn eine Domain-Konfiguration gefunden wurde, setze die URL
if ($domainConfig && isset($domainConfig['url'])) {
$domainUrl = $domainConfig['url'];
// URL-Generator konfigurieren
URL::forceRootUrl($domainUrl);
URL::forceScheme(parse_url($domainUrl, PHP_URL_SCHEME) ?: 'https');
// Asset-Root setzen
/** @var UrlGenerator $urlGenerator */
$urlGenerator = app('url');
$urlGenerator->useAssetOrigin($domainUrl);
// Config aktualisieren
config([
'app.url' => $domainUrl,
'app.asset_url' => $domainUrl,
]);
}
return $next($request);
}
}

View file

@ -19,13 +19,13 @@ class ThemeMiddleware
$path = $request->path();
// Theme-Switching über Subdomains
if (str_contains($host, 'b2in.test')) {
if (str_contains($host, 'b2in')) {
config(['app.theme' => 'b2in']);
} elseif (str_contains($host, 'b2a.test')) {
} elseif (str_contains($host, 'b2a') || str_contains($host, 'bridges2america')) {
config(['app.theme' => 'b2a']);
} elseif (str_contains($host, 'stileigentum.test')) {
} elseif (str_contains($host, 'stileigentum')) {
config(['app.theme' => 'stileigentum']);
} elseif (str_contains($host, 'style2own.test')) {
} elseif (str_contains($host, 'style2own')) {
config(['app.theme' => 'style2own']);
}
@ -41,7 +41,7 @@ class ThemeMiddleware
if (str_starts_with($path, 'b2in/')) {
config(['app.theme' => 'b2in']);
$request->server->set('REQUEST_URI', '/' . substr($path, 5)); // Entferne 'b2in/' vom Pfad
} elseif (str_starts_with($path, 'b2a/')) {
} elseif (str_starts_with($path, 'b2a/') || str_starts_with($path, 'bridges2america/')) {
config(['app.theme' => 'b2a']);
$request->server->set('REQUEST_URI', '/' . substr($path, 4)); // Entferne 'b2a/' vom Pfad
} elseif (str_starts_with($path, 'stileigentum/')) {

View file

@ -0,0 +1,292 @@
<?php
namespace App\Livewire\Admin\CMS;
use App\Models\DisplayVideo;
use App\Models\DisplayFooterContent;
use Livewire\Component;
use Livewire\Attributes\On;
use Illuminate\Support\Facades\File;
class CabinetDisplay extends Component
{
// Video-Verwaltung
public $videoId = null;
public $videoFilename = '';
public $videoTitle = '';
public $videoPosition = 25;
public $videoIsActive = true;
public $showVideoModal = false;
public $availableVideos = [];
// Footer-Content-Verwaltung
public $footerId = null;
public $footerHeadline = '';
public $footerSubline = '';
public $footerUrl = '';
public $footerIsActive = true;
public $showFooterModal = false;
public function mount()
{
$this->loadAvailableVideos();
}
/**
* Lädt alle verfügbaren Video-Dateien aus dem assets-Ordner
*/
public function loadAvailableVideos()
{
$assetsPath = public_path('_cabinet/assets');
if (File::exists($assetsPath)) {
$files = File::files($assetsPath);
$this->availableVideos = collect($files)
->map(fn($file) => $file->getFilename())
->filter(fn($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
->values()
->toArray();
}
}
// ========================================
// VIDEO-VERWALTUNG
// ========================================
public function openVideoModal($id = null)
{
if ($id) {
$video = DisplayVideo::findOrFail($id);
$this->videoId = $video->id;
$this->videoFilename = $video->filename;
$this->videoTitle = $video->title ?? '';
$this->videoPosition = $video->position;
$this->videoIsActive = $video->is_active;
} else {
$this->resetVideoForm();
}
$this->showVideoModal = true;
}
public function saveVideo()
{
$this->validate([
'videoFilename' => 'required|string',
'videoPosition' => 'required|integer|min:0|max:100',
], [
'videoFilename.required' => 'Bitte wählen Sie ein Video aus.',
'videoPosition.required' => 'Die Position ist erforderlich.',
'videoPosition.min' => 'Die Position muss zwischen 0 und 100 liegen.',
'videoPosition.max' => 'Die Position muss zwischen 0 und 100 liegen.',
]);
$data = [
'filename' => $this->videoFilename,
'title' => $this->videoTitle,
'position' => $this->videoPosition,
'is_active' => $this->videoIsActive,
];
if ($this->videoId) {
$video = DisplayVideo::findOrFail($this->videoId);
$video->update($data);
session()->flash('success', 'Video erfolgreich aktualisiert!');
} else {
$maxSortOrder = DisplayVideo::max('sort_order') ?? 0;
$data['sort_order'] = $maxSortOrder + 1;
DisplayVideo::create($data);
session()->flash('success', 'Video erfolgreich hinzugefügt!');
}
$this->closeVideoModal();
}
public function deleteVideo($id)
{
DisplayVideo::findOrFail($id)->delete();
session()->flash('success', 'Video erfolgreich gelöscht!');
}
public function toggleVideoStatus($id)
{
$video = DisplayVideo::findOrFail($id);
$video->update(['is_active' => !$video->is_active]);
}
public function moveVideo($id, $direction)
{
$video = DisplayVideo::findOrFail($id);
$currentOrder = $video->sort_order;
if ($direction === 'up' && $currentOrder > 0) {
$swapVideo = DisplayVideo::where('sort_order', $currentOrder - 1)->first();
if ($swapVideo) {
$video->update(['sort_order' => $currentOrder - 1]);
$swapVideo->update(['sort_order' => $currentOrder]);
}
} elseif ($direction === 'down') {
$swapVideo = DisplayVideo::where('sort_order', $currentOrder + 1)->first();
if ($swapVideo) {
$video->update(['sort_order' => $currentOrder + 1]);
$swapVideo->update(['sort_order' => $currentOrder]);
}
}
}
public function resetVideoForm()
{
$this->videoId = null;
$this->videoFilename = '';
$this->videoTitle = '';
$this->videoPosition = 25;
$this->videoIsActive = true;
}
public function closeVideoModal()
{
$this->showVideoModal = false;
$this->resetVideoForm();
}
// ========================================
// FOOTER-CONTENT-VERWALTUNG
// ========================================
public function openFooterModal($id = null)
{
if ($id) {
$footer = DisplayFooterContent::findOrFail($id);
$this->footerId = $footer->id;
$this->footerHeadline = $footer->headline;
$this->footerSubline = $footer->subline;
$this->footerUrl = $footer->url;
$this->footerIsActive = $footer->is_active;
} else {
$this->resetFooterForm();
}
$this->showFooterModal = true;
}
public function saveFooter()
{
$this->validate([
'footerHeadline' => 'required|string|max:255',
'footerSubline' => 'required|string|max:255',
'footerUrl' => 'nullable|url',
], [
'footerHeadline.required' => 'Die Überschrift ist erforderlich.',
'footerSubline.required' => 'Die Unterzeile ist erforderlich.',
'footerUrl.url' => 'Bitte geben Sie eine gültige URL ein.',
]);
$data = [
'headline' => $this->footerHeadline,
'subline' => $this->footerSubline,
'url' => $this->footerUrl ?: null,
'is_active' => $this->footerIsActive,
];
if ($this->footerId) {
$footer = DisplayFooterContent::findOrFail($this->footerId);
$footer->update($data);
// Short-Code generieren falls URL vorhanden aber noch kein Short-Code
if ($footer->url && !$footer->short_code) {
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
$footer->save();
}
session()->flash('success', 'Footer-Inhalt erfolgreich aktualisiert!');
} else {
$maxSortOrder = DisplayFooterContent::max('sort_order') ?? 0;
$data['sort_order'] = $maxSortOrder + 1;
$footer = DisplayFooterContent::create($data);
// Short-Code nur generieren wenn URL vorhanden
if ($footer->url) {
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
$footer->save();
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! Short-Link: ' . $footer->short_url);
} else {
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! (Ohne QR-Code)');
}
}
$this->closeFooterModal();
}
public function regenerateShortCode($id)
{
$footer = DisplayFooterContent::findOrFail($id);
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
$footer->save();
session()->flash('success', 'Short-Code wurde neu generiert!');
}
public function resetClicks($id)
{
$footer = DisplayFooterContent::findOrFail($id);
$footer->clicks = 0;
$footer->save();
session()->flash('success', 'Klick-Zähler wurde zurückgesetzt!');
}
public function deleteFooter($id)
{
DisplayFooterContent::findOrFail($id)->delete();
session()->flash('success', 'Footer-Inhalt erfolgreich gelöscht!');
}
public function toggleFooterStatus($id)
{
$footer = DisplayFooterContent::findOrFail($id);
$footer->update(['is_active' => !$footer->is_active]);
}
public function moveFooter($id, $direction)
{
$footer = DisplayFooterContent::findOrFail($id);
$currentOrder = $footer->sort_order;
if ($direction === 'up' && $currentOrder > 0) {
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder - 1)->first();
if ($swapFooter) {
$footer->update(['sort_order' => $currentOrder - 1]);
$swapFooter->update(['sort_order' => $currentOrder]);
}
} elseif ($direction === 'down') {
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder + 1)->first();
if ($swapFooter) {
$footer->update(['sort_order' => $currentOrder + 1]);
$swapFooter->update(['sort_order' => $currentOrder]);
}
}
}
public function resetFooterForm()
{
$this->footerId = null;
$this->footerHeadline = '';
$this->footerSubline = '';
$this->footerUrl = '';
$this->footerIsActive = true;
}
public function closeFooterModal()
{
$this->showFooterModal = false;
$this->resetFooterForm();
}
public function render()
{
$videos = DisplayVideo::orderBy('sort_order')->get();
$footerContents = DisplayFooterContent::orderBy('sort_order')->get();
return view('livewire.admin.c-m-s.cabinet-display', [
'videos' => $videos,
'footerContents' => $footerContents,
]);
}
}

View file

@ -16,7 +16,18 @@ class Header extends Component
$this->domainName = \App\Helpers\ThemeHelper::getDomainName();
$this->domainUrl = \App\Helpers\ThemeHelper::getDomainUrl();
$theme = config('app.theme', 'b2in');
$this->content = config("content.themes.{$theme}.header", []);
$this->content = config("content.themes.{$theme}.header", [
'portal_login' => 'Portal Login',
'navigation' => []
]);
// Ensure required keys exist
if (!isset($this->content['portal_login'])) {
$this->content['portal_login'] = 'Portal Login';
}
if (!isset($this->content['navigation']) || !is_array($this->content['navigation'])) {
$this->content['navigation'] = [];
}
}
public function toggleMobileMenu()

View file

@ -0,0 +1,89 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DisplayFooterContent extends Model
{
protected $fillable = [
'headline',
'subline',
'url',
'short_code',
'clicks',
'sort_order',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
'sort_order' => 'integer',
'clicks' => 'integer',
];
/**
* Boot-Methode für automatische Short-Code-Generierung
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
// Short-Code nur generieren wenn URL vorhanden ist
if ($model->url && empty($model->short_code)) {
$model->short_code = self::generateUniqueShortCode();
}
});
}
/**
* Generiert einen eindeutigen Short-Code
*/
public static function generateUniqueShortCode(): string
{
do {
// Generiere einen 6-stelligen alphanumerischen Code
$code = strtolower(substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 6));
} while (self::where('short_code', $code)->exists());
return $code;
}
/**
* Gibt die Short-URL zurück (dynamisch je nach Umgebung)
*/
public function getShortUrlAttribute(): string
{
$basePath = config('display.base_path', '_display-b2in-eu');
$subdomain = config('display.subdomain');
$domain = config('display.domain', 'b2in.eu');
// Wenn Subdomain konfiguriert ist, verwende diese (Live-Server)
if ($subdomain) {
// Nutze HTTPS für Produktion
$protocol = config('app.env') === 'production' ? 'https' : 'http';
return "{$protocol}://{$subdomain}.{$domain}/go.php?z={$this->short_code}";
}
// Ansonsten verwende APP_URL + Base-Path (Testserver)
$baseUrl = rtrim(config('app.url'), '/');
return "{$baseUrl}/{$basePath}/go.php?z={$this->short_code}";
}
/**
* Erhöht den Klick-Zähler
*/
public function incrementClicks(): void
{
$this->increment('clicks');
}
/**
* Scope für aktive Footer-Inhalte in sortierter Reihenfolge
*/
public function scopeActive($query)
{
return $query->where('is_active', true)->orderBy('sort_order');
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DisplayVideo extends Model
{
protected $fillable = [
'filename',
'title',
'position',
'sort_order',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
'position' => 'integer',
'sort_order' => 'integer',
];
/**
* Scope für aktive Videos in sortierter Reihenfolge
*/
public function scopeActive($query)
{
return $query->where('is_active', true)->orderBy('sort_order');
}
/**
* Gibt den vollständigen Pfad zum Video zurück
*/
public function getFullPathAttribute(): string
{
return "assets/{$this->filename}";
}
}

View file

@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Partner extends Model
{
@ -13,9 +14,22 @@ class Partner extends Model
protected $fillable = [
'company_name',
'display_name',
'slug',
'type',
'brand',
'hub_id',
'parent_partner_id',
'salutation',
'first_name',
'last_name',
'street',
'house_number',
'zip',
'city',
'country',
'phone',
'website',
'description',
'logo_url',
'is_active',
@ -50,6 +64,46 @@ class Partner extends Model
return $this->hasMany(User::class);
}
/**
* Für Kunden: Zugeordneter Makler/Händler (Parent-Partner)
*/
public function parentPartner(): BelongsTo
{
return $this->belongsTo(Partner::class, 'parent_partner_id');
}
/**
* Für Makler/Händler: Zugeordnete Kunden (Child-Partners)
*/
public function childPartners(): HasMany
{
return $this->hasMany(Partner::class, 'parent_partner_id');
}
/**
* Alias für parentPartner (für bessere Lesbarkeit im Code)
*/
public function broker(): BelongsTo
{
return $this->parentPartner();
}
/**
* Alias für childPartners (für bessere Lesbarkeit im Code)
*/
public function customers(): HasMany
{
return $this->childPartners();
}
/**
* Ein Partner (Manufacturer) kann eine Marke haben
*/
public function brand(): HasOne
{
return $this->hasOne(Brand::class);
}
// TODO: Später die Beziehung zu Products hinzufügen
// public function products(): HasMany
// {

View file

@ -0,0 +1,87 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RegistrationCode extends Model
{
use HasFactory;
public const STATUS_AVAILABLE = 'available';
public const STATUS_USED = 'used';
public const STATUS_EXPIRED = 'expired';
protected $fillable = [
'code',
'role',
'name',
'status',
'broker_partner_id',
'partner_id',
'assigned_to_code_id',
'used_by_user_id',
'used_at',
'expires_at',
'metadata',
];
protected $casts = [
'metadata' => 'array',
'expires_at' => 'datetime',
'used_at' => 'datetime',
];
public function broker(): BelongsTo
{
return $this->belongsTo(Partner::class, 'broker_partner_id');
}
public function partner(): BelongsTo
{
return $this->belongsTo(Partner::class);
}
public function usedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'used_by_user_id');
}
public function assignedToCode(): BelongsTo
{
return $this->belongsTo(RegistrationCode::class, 'assigned_to_code_id');
}
public function scopeAvailable($query)
{
return $query->where('status', self::STATUS_AVAILABLE);
}
public function isAvailable(): bool
{
if ($this->status !== self::STATUS_AVAILABLE) {
return false;
}
if ($this->expires_at && now()->greaterThan($this->expires_at)) {
return false;
}
return true;
}
public function markUsed(?User $user = null): void
{
$this->status = self::STATUS_USED;
$this->used_at = now();
if ($user) {
$this->used_by_user_id = $user->id;
if ($user->partner_id && !$this->partner_id) {
$this->partner_id = $user->partner_id;
}
}
$this->save();
}
}

View file

@ -2,8 +2,11 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Notifications\CustomResetPasswordNotification;
use App\Notifications\CustomVerifyEmailNotification;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
@ -11,11 +14,12 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Authenticatable
class User extends Authenticatable implements MustVerifyEmail
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable;
use HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable, SoftDeletes;
/**
* The attributes that are mass assignable.
@ -25,6 +29,7 @@ class User extends Authenticatable
protected $fillable = [
'partner_id',
'name',
'display_name',
'email',
'password',
'email_verified_at',
@ -49,6 +54,7 @@ class User extends Authenticatable
{
return [
'email_verified_at' => 'datetime',
'deleted_at' => 'datetime',
'password' => 'hashed',
];
}
@ -57,6 +63,15 @@ class User extends Authenticatable
{
return $this->belongsTo(Partner::class);
}
/**
* Get the registration code used by this user
*/
public function registrationCode(): HasOne
{
return $this->hasOne(RegistrationCode::class, 'used_by_user_id');
}
/**
* Get the user's initials
*/
@ -67,4 +82,56 @@ class User extends Authenticatable
->map(fn(string $name) => Str::of($name)->substr(0, 1))
->implode('');
}
/**
* Anonymize user data (for users with dependencies)
*/
public function anonymize(): void
{
$this->update([
'name' => 'Gelöschter Benutzer #' . $this->id,
'display_name' => null,
'email' => 'deleted_' . $this->id . '@anonymized.local',
'password' => bcrypt(Str::random(64)),
]);
// Entferne alle Rollen
$this->syncRoles([]);
// Soft Delete
$this->delete();
}
/**
* Check if user has dependencies that require anonymization instead of deletion
*/
public function hasDependencies(): bool
{
// TODO: Später erweitern mit weiteren Verknüpfungen
// Beispiele: Orders, Projects, Documents, etc.
// Aktuell: Prüfe ob Partner existiert
return $this->partner_id !== null;
}
/**
* Send the password reset notification.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token): void
{
$this->notify(new CustomResetPasswordNotification($token));
}
/**
* Send the email verification notification.
*
* @return void
*/
public function sendEmailVerificationNotification(): void
{
$this->notify(new CustomVerifyEmailNotification);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Notifications;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Notifications\Messages\MailMessage;
class CustomResetPasswordNotification extends ResetPassword
{
/**
* Get the reset password notification mail message for the given URL.
*
* @param string $url
* @return \Illuminate\Notifications\Messages\MailMessage
*/
protected function buildMailMessage($url)
{
$expiryMinutes = config('auth.passwords.' . config('auth.defaults.passwords') . '.expire');
return (new MailMessage)
->subject('Passwort zurücksetzen - ' . config('app.name'))
->greeting('Hallo!')
->line('Sie erhalten diese E-Mail, weil wir eine Anfrage zum Zurücksetzen des Passworts für Ihr Konto erhalten haben.')
->line('Klicken Sie auf den folgenden Button, um ein neues Passwort zu vergeben:')
->action('Passwort zurücksetzen', $url)
->line('Dieser Link ist **' . $expiryMinutes . ' Minuten** gültig.')
->line('Falls Sie keine Passwort-Zurücksetzung angefordert haben, ist keine weitere Aktion erforderlich. Ihr Passwort bleibt unverändert.')
->salutation('Mit freundlichen Grüßen, ' . PHP_EOL . 'Ihr **' . config('app.name') . '** Team');
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Notifications;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\URL;
class CustomVerifyEmailNotification extends VerifyEmail
{
/**
* Get the verify email notification mail message for the given URL.
*
* @param string $url
* @return \Illuminate\Notifications\Messages\MailMessage
*/
protected function buildMailMessage($url)
{
return (new MailMessage)
->subject('E-Mail-Adresse bestätigen - ' . config('app.name'))
->greeting('Willkommen bei ' . config('app.name') . '!')
->line('Vielen Dank für Ihre Registrierung!')
->line('Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Button klicken:')
->action('E-Mail-Adresse bestätigen', $url)
->line('Dieser Bestätigungslink läuft in **60 Minuten** ab.')
->line('Falls Sie kein Konto bei uns erstellt haben, ignorieren Sie bitte diese E-Mail.')
->salutation('Mit freundlichen Grüßen, ' . PHP_EOL . 'Ihr **' . config('app.name') . '** Team');
}
}

View file

@ -2,7 +2,9 @@
namespace App\Providers;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
@ -55,23 +57,55 @@ class ThemeServiceProvider extends ServiceProvider
$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([
'app.theme' => $domainConfig['theme'],
'app.view_prefix' => $domainConfig['view_prefix'],
'app.domain_name' => $domainConfig['domain_name'],
'app.url' => $domainConfig['url'],
'app.asset_url' => $assetUrl, // Dynamische Asset-URL für die aktuelle Domain
]);
// URL-Generator für die aktuelle Domain konfigurieren
// Dies ist wichtig, damit asset() und url() die richtige Domain verwenden
URL::forceRootUrl($domainConfig['url']);
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 */
$urlGenerator = app('url');
$urlGenerator->useAssetOrigin($assetUrl);
// Spezifischere Daten für die Views verfügbar machen
View::share('theme', $domainConfig['theme']);
View::share('viewPrefix', $domainConfig['view_prefix']);
View::share('domainName', $domainConfig['domain_name']);
View::share('domainConfig', $domainConfig);
View::share('domainUrl', $domainConfig['url']);
View::share('assetUrl', $assetUrl);
// Vite-Assets-Konfiguration für die aktuelle Domain
if (! app()->runningInConsole() && isset($domainConfig['assets_dir'])) {
Vite::useBuildDirectory($domainConfig['assets_dir']);
if (! app()->runningInConsole()) {
if (isset($domainConfig['assets_dir'])) {
Vite::useBuildDirectory($domainConfig['assets_dir']);
}
if (app()->environment('local')) {
// Entwicklung: Vite Dev Server mit HMR
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.b2in.test');
Vite::useHotFile(public_path('hot'));
config(['app.vite_dev_server_url' => $viteDevServerUrl]);
View::share('viteDevServerUrl', $viteDevServerUrl);
} else {
// Produktion: Assets von der aktuellen Domain laden (kein CORS nötig)
Vite::useScriptTagAttributes(['crossorigin' => false]);
Vite::useStyleTagAttributes(['crossorigin' => false]);
}
}
}
}