schneespur/schneespur/app/Http/Controllers/InstallerController.php
Michael 7e27270626 Restructure: move source into schneespur/ subdirectory, remove vendor/release from tracking
- Root: README.md, LICENSE, INSTALL.de.md, INSTALL.en.md only
- schneespur/: all application source code
- Added .gitignore for vendor/, node_modules/, release/, .env, build artifacts
- Removed vendor/ and release/ from git tracking (15,699 files)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 17:02:37 +00:00

411 lines
13 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Enums\UserRole;
use App\Models\Setting;
use App\Models\User;
use App\Services\Installer\EnvFileWriter;
use App\Services\Installer\InstallLockManager;
use App\Services\Installer\MigrationRunner;
use App\Services\Installer\PreflightChecker;
use App\Services\Installer\StorageConfigurator;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Mail;
use Illuminate\View\View;
use PDO;
class InstallerController extends Controller
{
public function __construct(
private EnvFileWriter $envWriter,
private PreflightChecker $preflightChecker,
private MigrationRunner $migrationRunner,
private StorageConfigurator $storageConfigurator,
private InstallLockManager $lockManager,
) {}
// --- Locale switcher (works on any installer step) ---
public function switchLocale(Request $request, string $locale): RedirectResponse
{
if (in_array($locale, ['de', 'en'], true)) {
$request->session()->put('installer_locale', $locale);
}
return redirect($request->headers->get('referer') ?: route('install.welcome'));
}
// --- Step 1: Welcome ---
public function showWelcome(Request $request): View
{
$this->autoDetectAppUrl($request);
return view('installer.step1-welcome', ['currentStep' => 1]);
}
public function processWelcome(): RedirectResponse
{
return redirect()->route('install.preflight');
}
// --- Step 2: Preflight (was Step 3) ---
// --- Step 3: Database (was Step 2) ---
public function showDatabase(): View
{
return view('installer.step2-database', [
'currentStep' => 3,
'env_content' => null,
]);
}
public function storeDatabase(Request $request): RedirectResponse
{
$validated = $request->validate([
'db_host' => 'required|string',
'db_port' => 'required|integer|min:1|max:65535',
'db_database' => 'required|string',
'db_username' => 'required|string',
'db_password' => 'nullable|string',
]);
$password = $validated['db_password'] ?? '';
try {
new PDO(
"mysql:host={$validated['db_host']};port={$validated['db_port']};dbname={$validated['db_database']}",
$validated['db_username'],
$password,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
);
} catch (\PDOException $e) {
return redirect()->route('install.database')
->withInput()
->withErrors(['db_connection' => __('install.error_db_connection') . ' (' . $e->getMessage() . ')']);
}
$envValues = [
'DB_CONNECTION' => 'mysql',
'DB_HOST' => $validated['db_host'],
'DB_PORT' => (string) $validated['db_port'],
'DB_DATABASE' => $validated['db_database'],
'DB_USERNAME' => $validated['db_username'],
'DB_PASSWORD' => $password,
];
if (! $this->envWriter->isWritable()) {
$this->envWriter->setMany($envValues);
Artisan::call('config:clear');
if (! $this->envWriter->isWritable()) {
return redirect()->route('install.database')
->withInput()
->with('env_content', $this->envWriter->getFullContent())
->withErrors(['env_write' => __('install.error_env_write')]);
}
}
$this->envWriter->setMany($envValues);
Artisan::call('config:clear');
return redirect()->route('install.migrations');
}
public function showPreflight(): View
{
return view('installer.step3-preflight', [
'currentStep' => 2,
'checks' => $this->preflightChecker->check(),
'hasCritical' => $this->preflightChecker->hasCriticalFailures(),
]);
}
public function processPreflight(): RedirectResponse
{
if ($this->preflightChecker->hasCriticalFailures()) {
return redirect()->route('install.preflight')
->withErrors(['preflight' => 'Kritische Voraussetzungen nicht erfüllt.']);
}
return redirect()->route('install.database');
}
// --- Step 4: Migrations ---
public function showMigrations(): View
{
return view('installer.step4-migrations', ['currentStep' => 4]);
}
public function runMigrations(): RedirectResponse
{
$result = $this->migrationRunner->run();
if (! $result['success']) {
return redirect()->route('install.migrations')
->withErrors(['migration' => __('install.error_migration_main')])
->with('migration_output', $result['error'] ?? $result['output']);
}
$this->envWriter->setMany([
'SESSION_DRIVER' => 'database',
'CACHE_STORE' => 'database',
]);
Artisan::call('config:clear');
return redirect()->route('install.config');
}
// --- Step 5: Config ---
public function showConfig(Request $request): View
{
$detectedUrl = $request->schemeAndHttpHost();
return view('installer.step5-config', [
'currentStep' => 5,
'app_url' => $this->envWriter->get('APP_URL') ?: $detectedUrl,
'timezone' => $this->envWriter->get('APP_DISPLAY_TIMEZONE') ?: 'Europe/Berlin',
]);
}
public function storeConfig(Request $request): RedirectResponse
{
$validated = $request->validate([
'app_url' => 'required|url',
'timezone' => 'required|string|timezone:all',
'locale' => 'required|string|in:de,en',
]);
$this->envWriter->setMany([
'APP_URL' => $validated['app_url'],
'APP_DISPLAY_TIMEZONE' => $validated['timezone'],
'APP_LOCALE' => $validated['locale'],
]);
Artisan::call('config:clear');
$brand = $validated['locale'] === 'de' ? 'schneespur' : 'wintertrace';
try {
Setting::set('app_url', $validated['app_url']);
Setting::set('display_timezone', $validated['timezone']);
Setting::set('locale', $validated['locale']);
Setting::set('app_brand', $brand);
} catch (\Exception) {
// Settings table may not exist yet in edge cases — .env is the primary store
}
return redirect()->route('install.storage');
}
// --- Step 6: Storage ---
public function showStorage(): View
{
return view('installer.step6-storage', [
'currentStep' => 6,
'results' => null,
]);
}
public function runStorage(): View
{
$results = $this->storageConfigurator->runAll();
return view('installer.step6-storage', [
'currentStep' => 6,
'results' => $results,
]);
}
// --- Step 7: Admin ---
public function showAdmin(): View
{
return view('installer.step7-admin', ['currentStep' => 7]);
}
public function storeAdmin(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'password' => 'required|string|min:8|confirmed',
]);
User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => $validated['password'],
])->forceFill(['role' => UserRole::Admin])->save();
$this->lockManager->lock();
try {
Setting::set('installed_at', now()->toIso8601String());
} catch (\Exception) {
// Fallback: lock file is the authoritative indicator
}
Artisan::call('config:clear');
return redirect()->route('install.mail');
}
// --- Step 8: Mail ---
public function showMail(): View
{
return view('installer.step8-mail', ['currentStep' => 8]);
}
public function sendTestMail(Request $request): RedirectResponse
{
$validated = $request->validate([
'mail_host' => 'required|string',
'mail_port' => 'required|integer',
'mail_username' => 'nullable|string',
'mail_password' => 'nullable|string',
'mail_encryption' => 'nullable|string|in:tls,ssl,null',
'mail_from_address' => 'required|email',
'mail_from_name' => 'required|string',
'test_recipient' => 'required|email',
]);
$schemeMap = ['tls' => '', 'ssl' => 'smtps', 'null' => ''];
$mailScheme = $schemeMap[$validated['mail_encryption'] ?? 'null'] ?? '';
$this->envWriter->setMany([
'MAIL_MAILER' => 'smtp',
'MAIL_HOST' => $validated['mail_host'],
'MAIL_PORT' => (string) $validated['mail_port'],
'MAIL_SCHEME' => $mailScheme,
'MAIL_USERNAME' => $validated['mail_username'] ?? '',
'MAIL_PASSWORD' => $validated['mail_password'] ?? '',
'MAIL_FROM_ADDRESS' => $validated['mail_from_address'],
'MAIL_FROM_NAME' => $validated['mail_from_name'],
]);
Artisan::call('config:clear');
config([
'mail.default' => 'smtp',
'mail.mailers.smtp.host' => $validated['mail_host'],
'mail.mailers.smtp.port' => $validated['mail_port'],
'mail.mailers.smtp.username' => $validated['mail_username'],
'mail.mailers.smtp.password' => $validated['mail_password'],
'mail.mailers.smtp.scheme' => $mailScheme ?: null,
'mail.from.address' => $validated['mail_from_address'],
'mail.from.name' => $validated['mail_from_name'],
]);
try {
Mail::raw(__('install.mail_test_body', ['brand' => brand()]), function ($message) use ($validated) {
$message->to($validated['test_recipient'])
->subject(brand() . ' — ' . __('install.mail_test_subject'));
});
return redirect()->route('install.cron')
->with('flash_test_mail', __('install.flash_test_mail', ['email' => $validated['test_recipient']]));
} catch (\Exception $e) {
return redirect()->route('install.mail')
->withInput()
->withErrors(['mail' => $e->getMessage()]);
}
}
public function skipMail(): RedirectResponse
{
return redirect()->route('install.cron');
}
// --- Step 9: Cron ---
public function showCron(): View
{
$cronLine = '* * * * * ' . $this->detectPhpCli() . ' ' . base_path('artisan') . ' schedule:run >> /dev/null 2>&1';
$cronActive = cache()->has('cron.last_run');
return view('installer.step9-cron', [
'currentStep' => 9,
'cronLine' => $cronLine,
'cronActive' => $cronActive,
]);
}
public function testCron(): RedirectResponse
{
try {
Artisan::call('schedule:run');
} catch (\Exception) {
// Not critical
}
cache()->put('cron.last_run', now());
return redirect()->route('install.cron')
->with('cron_test_success', true);
}
public function skipCron(): RedirectResponse
{
return redirect()->route('install.done');
}
// --- Helpers ---
private function detectPhpCli(): string
{
$binary = PHP_BINARY;
if (str_contains($binary, 'fpm') || str_contains($binary, 'cgi')) {
$version = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
foreach (["/usr/bin/php{$version}", "/usr/bin/php", "/usr/local/bin/php"] as $candidate) {
if (is_executable($candidate)) {
return $candidate;
}
}
}
return $binary;
}
private function autoDetectAppUrl(Request $request): void
{
$detected = $request->getSchemeAndHttpHost();
$current = $this->envWriter->get('APP_URL');
if (! $current || $current === 'http://localhost') {
$this->envWriter->set('APP_URL', $detected);
config(['app.url' => $detected]);
url()->forceRootUrl($detected);
if ($request->isSecure()) {
url()->forceScheme('https');
}
}
}
// --- Done ---
public function showDone(): View
{
if (! $this->lockManager->isLocked()) {
return view('installer.step1-welcome', ['currentStep' => 1]);
}
$admin = User::where('role', UserRole::Admin)->first();
return view('installer.done', [
'currentStep' => 10,
'appUrl' => $this->envWriter->get('APP_URL') ?: url('/'),
'adminEmail' => $admin?->email ?? '—',
'mailConfigured' => ! empty($this->envWriter->get('MAIL_HOST')),
]);
}
}