first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
Kevin Adametz 2025-10-20 17:53:02 +02:00
commit 405df0a122
3083 changed files with 69203 additions and 0 deletions

View file

@ -0,0 +1,40 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique(User::class),
],
'password' => $this->passwordRules(),
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Validation\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
*/
protected function passwordRules(): array
{
return ['required', 'string', Password::default(), 'confirmed'];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* Validate and reset the user's forgotten password.
*
* @param array<string, string> $input
*/
public function reset(User $user, array $input): void
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* Validate and update the user's password.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
'password' => $this->passwordRules(),
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class GenerateDomainFavicons extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'domains:generate-favicons';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generiere Favicons für alle konfigurierten Domains';
/**
* Execute the console command.
*/
public function handle()
{
$domains = config('domains');
$faviconDir = public_path('img/favicons');
// Erstelle das Favicon-Verzeichnis, wenn es nicht existiert
if (!File::exists($faviconDir)) {
File::makeDirectory($faviconDir, 0755, true);
$this->info("Verzeichnis {$faviconDir} erstellt.");
}
// Erstelle eine Liste der Themes, für die wir Favicons erstellen müssen
$themes = [];
foreach ($domains as $domain => $config) {
if (is_array($config) && isset($config['theme'])) {
$themes[$config['theme']] = [
'domain' => $domain,
'name' => $config['name'] ?? 'Website',
'color' => $config['color_scheme']['primary'] ?? '#000000',
];
}
}
// Erstelle die Favicons
foreach ($themes as $theme => $data) {
$faviconPath = "{$faviconDir}/{$theme}-favicon.ico";
// Wenn die Datei bereits existiert, frage, ob sie überschrieben werden soll
if (File::exists($faviconPath) && !$this->confirm("Favicon für '{$theme}' existiert bereits. Überschreiben?")) {
$this->info("Favicon für '{$theme}' übersprungen.");
continue;
}
// Hier könntest du mit einer externen Bibliothek einen Favicon erstellen
// Da dies aber komplex sein kann, erstellen wir erstmal nur Platzhalter-Dateien
File::put($faviconPath, '');
$this->info("Platzhalter-Favicon für '{$theme}' erstellt: {$faviconPath}");
$this->comment("Ersetze diese Datei mit einem echten Favicon für {$data['domain']} ({$data['name']})");
}
$this->info('Favicons wurden erfolgreich erstellt!');
$this->line('Denke daran, die Platzhalter-Dateien mit echten Favicons zu ersetzen.');
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
// Import der Models für die NEUE Struktur
use App\Models\User as NewUser;
use App\Models\Article as NewArticle;
use App\Models\Brand;
// Import der Models für die ALTE Struktur
use App\Models\Legacy\Presseecho\User as LegacyUser;
use App\Models\Legacy\Presseecho\Article as LegacyArticle;
class MigratePresseechoData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:presseecho';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrates all data from the old presseecho.de database to the new central database';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting migration for presseecho.de...');
// Holen der Brand-ID für Presseecho aus der neuen DB
$brand = Brand::where('domain', 'presseecho.de')->first();
if (!$brand) {
$this->error('Brand "presseecho.de" not found in the new database. Please seed brands first.');
return 1;
}
// Deaktivieren von Foreign Key Checks für reibungsloses Einfügen
DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=0;');
// Starten der einzelnen Migrationsschritte
$this->migrateUsers();
$this->migrateArticles($brand->id);
// $this->migrateCategories($brand->id); // Weitere Schritte hier hinzufügen
// Reaktivieren der Foreign Key Checks
DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=1;');
$this->info('Migration for presseecho.de completed successfully!');
return 0;
}
private function migrateUsers()
{
$this->line('Migrating users...');
$progressBar = $this->output->createProgressBar(LegacyUser::count());
$progressBar->start();
// Chunking verwenden, um den Speicher bei vielen Nutzern zu schonen
LegacyUser::chunk(200, function ($legacyUsers) use ($progressBar) {
foreach ($legacyUsers as $legacyUser) {
// Prüfen, ob der Nutzer (anhand der E-Mail) bereits in der neuen DB existiert
$existingUser = NewUser::where('email', $legacyUser->email)->first();
if (!$existingUser) {
// Nutzer existiert noch nicht -> neu anlegen
NewUser::create([
'name' => $legacyUser->name,
'email' => $legacyUser->email,
'password' => $legacyUser->password, // Annahme: Passwörter sind bereits gehasht. Wenn nicht: Hash::make(Str::random(16)) und Passwort-Reset erzwingen
'email_verified_at' => $legacyUser->created_at, // oder ein anderer Zeitstempel
'created_at' => $legacyUser->created_at,
'updated_at' => $legacyUser->updated_at,
// ... weitere Felder mappen
]);
}
$progressBar->advance();
}
});
$progressBar->finish();
$this->newLine(2);
}
private function migrateArticles(int $brandId)
{
$this->line('Migrating articles...');
$progressBar = $this->output->createProgressBar(LegacyArticle::count());
$progressBar->start();
LegacyArticle::chunk(200, function ($legacyArticles) use ($brandId, $progressBar) {
foreach ($legacyArticles as $legacyArticle) {
// Den zugehörigen Nutzer in der NEUEN Datenbank finden
$author = NewUser::where('email', $legacyArticle->author_email)->first(); // Annahme: Artikel hat eine Autoren-E-Mail
if (!$author) {
$this->warn("Skipping article ID {$legacyArticle->id}, author with email {$legacyArticle->author_email} not found.");
continue; // Artikel überspringen, wenn kein Autor gefunden wurde
}
// Neuen Artikel in der zentralen DB erstellen
$newArticle = NewArticle::create([
'user_id' => $author->id,
'title' => $legacyArticle->title,
'slug' => $legacyArticle->slug, // oder neu generieren: Str::slug($legacyArticle->title)
'content' => $legacyArticle->body, // Spaltennamen anpassen
'created_at' => $legacyArticle->created_at,
'updated_at' => $legacyArticle->updated_at,
// ... weitere Felder mappen
]);
// Den Artikel mit der Plattform "Presseecho" verknüpfen
// Dies fügt den Eintrag in die Pivot-Tabelle `article_platform` ein
$newArticle->platforms()->attach($brandId, [
'status' => 'published', // oder den alten Status übernehmen
'published_at' => $legacyArticle->published_date,
// eventuell weitere Pivot-Daten
]);
$progressBar->advance();
}
});
$progressBar->finish();
$this->newLine(2);
}
}

36
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
\App\Console\Commands\GenerateDomainFavicons::class,
];
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

117
app/Helpers/ThemeHelper.php Normal file
View file

@ -0,0 +1,117 @@
<?php
namespace App\Helpers;
class ThemeHelper
{
/**
* Get the logo path based on the current theme
*/
public static function getLogoPath(string $type = 'positive'): string
{
$theme = config('app.theme', 'pr-copilot');
$logoMap = [
'portal' => [
'positive' => 'img/logos/portal-logo-positive.svg',
'negative' => 'img/logos/portal-logo-negative.svg'
],
'presseecho' => [
'positive' => 'img/logos/presseecho-logo-positiv.svg',
'negative' => 'img/logos/presseecho-logo-negativ.svg'
],
'businessportal24' => [
'positive' => 'img/logos/businessportal24-logo-positiv.svg',
'negative' => 'img/logos/businessportal24-logo-negativ.svg'
]
];
return $logoMap[$theme][$type] ?? $logoMap['pr-copilot'][$type];
}
/**
* Get the favicon path based on the current theme
*/
public static function getFaviconPath(): string
{
$theme = config('app.theme', 'portal');
return "img/favicons/{$theme}-favicon.ico";
}
/**
* Get the CSS theme file based on the current theme
*/
public static function getThemeCssPath(): string
{
$theme = config('app.theme', 'portal');
return "resources/css/web/theme-{$theme}.css";
}
/**
* Get domain-specific configuration
*/
public static function getDomainConfig(): array
{
$theme = config('app.theme', 'portal');
return config("domains.domains.{$theme}", []);
}
/**
* Get primary color for the current theme
*/
public static function getPrimaryColor(): string
{
$config = self::getDomainConfig();
return $config['color_scheme']['primary'] ?? '#526266';
}
/**
* Get secondary color for the current theme
*/
public static function getSecondaryColor(): string
{
$config = self::getDomainConfig();
return $config['color_scheme']['secondary'] ?? '#82a0a7';
}
/**
* Get primary font family for the current theme
*/
public static function getFont(): string
{
$config = self::getDomainConfig();
return $config['font'] ?? 'Montserrat';
}
/**
* Get domain name for the current theme
*/
public static function getDomainName(): string
{
$config = self::getDomainConfig();
return $config['domain_name'] ?? 'pr-copilot.test';
}
public static function getDomainUrl(): string
{
$config = self::getDomainConfig();
return $config['url'] ?? config('app.url');
}
/**
* Get the asset URL (Vite dev server) for the current theme
*/
public static function getAssetUrl(): string
{
$theme = config('app.theme', 'portal');
$assetUrlMap = [
'portal' => 'https://assets.pr-copilot.test',
'presseecho' => 'https://assets.presseecho.test',
'businessportal24' => 'https://assets.businessportal24.test',
];
return $assetUrlMap[$theme] ?? 'https://assets.pr-copilot.test';
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
/** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */
$user = $request->user();
event(new Verified($user));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Actions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class Logout
{
/**
* Log the current user out of the application.
*/
public function __invoke()
{
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
return redirect('/');
}
}

62
app/Models/User.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, Notifiable, HasRoles, TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
/**
* Get the user's initials
*/
public function initials(): string
{
return Str::of($this->name)
->explode(' ')
->map(fn (string $name) => Str::of($name)->substr(0, 1))
->implode('');
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Force HTTPS when running behind a proxy (like Traefik)
if ($this->app->environment('local', 'production')) {
URL::forceScheme('https');
}
// Trust proxies for correct request detection
$this->app['request']->server->set('HTTPS', 'on');
// Set dynamic asset URL based on current domain/theme
// This is needed for Vite to use the correct asset subdomain
//config(['app.asset_url' => 'https://assets.businessportal24.test']);
//$this->setDynamicAssetUrl();
}
/**
* Set the asset URL dynamically based on the current theme
*/
protected function setDynamicAssetUrl(): void
{
try {
$assetUrl = \App\Helpers\ThemeHelper::getAssetUrl();
config(['app.asset_url' => $assetUrl]);
} catch (\Exception $e) {
// Fallback to default if theme detection fails
config(['app.asset_url' => 'https://assets.pr-copilot.test']);
}
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\Fortify;
use Livewire\Volt\Volt;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Nur die notwendigen Fortify-Aktionen behalten
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class);
// Keine View-Definitionen, da wir Volt verwenden
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
class ThemeServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
// Registriere die domains.php Konfigurationsdatei
$this->mergeConfigFrom(
base_path('config/domains.php'),
'domains'
);
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$host = Request::getHost(); // is domain_name
$themeOverride = Request::get('theme'); // Allow theme override via URL parameter
// Standard-Werte für Domain, die nicht in der Konfiguration sind
$domainConfig = [
'name' => config('app.name'),
'theme' => 'portal',
'view_prefix' => 'portal',
'assets_dir' => 'build/portal',
'url' => config('app.url'),
'domain_name' => config('app.domain_name'),
];
// Lade die Domain-Konfiguration
$confiDomains = config('domains.domains');
// Suche nach der aktuellen Domain in der Konfiguration
foreach ($confiDomains as $name => $config) {
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]);
}
// 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' => $domainConfig['asset_url'] ?? $domainConfig['url'],
]);
// 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']);
// Vite-Assets-Konfiguration für die aktuelle Domain
if (! app()->runningInConsole() && isset($domainConfig['assets_dir'])) {
Vite::useBuildDirectory($domainConfig['assets_dir']);
// Setze die Asset-URL für Vite Dev-Server
if (isset($domainConfig['asset_url'])) {
// Erstelle temporäre Hot-File mit der richtigen Asset-URL
$hotFile = public_path('hot');
if (file_exists($hotFile)) {
file_put_contents($hotFile, $domainConfig['asset_url']);
}
Vite::useHotFile($hotFile);
}
}
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Volt\Volt;
class VoltServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Volt::mount([
config('livewire.view_path', resource_path('views/livewire')),
resource_path('views/pages'),
]);
}
}