Add guided installer for SaaS application setup
- Updated README.md to include instructions for guided installation on webspace without shell access. - Enhanced installationshandbuch.md with detailed steps for using the guided installer. - Modified saas-app/README.md to summarize the installation process using the new installer. - Generated SQL migration bundle now includes timestamp for better tracking. - Introduced public/install/index.php as the entry point for the guided installer. - Updated public/index.php to reflect changes in installation steps and added links to the installer. - Refactored migration bundle generation and SQL execution scripts for improved error handling and modularity. - Implemented session-based CSRF protection in the installer. - Added form handling for environment variable configuration and migration execution in the installer. - Created a user-friendly HTML interface for the installer with status messages and error handling.
This commit is contained in:
@@ -2,54 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$projectRoot = dirname(__DIR__);
|
||||
$migrationDir = $projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
|
||||
$outputPath = $argv[1] ?? (
|
||||
$projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . 'all-migrations.sql'
|
||||
);
|
||||
require_once __DIR__ . '/support.php';
|
||||
|
||||
if (!is_dir($migrationDir)) {
|
||||
fwrite(STDERR, "Migrationsverzeichnis nicht gefunden: {$migrationDir}" . PHP_EOL);
|
||||
exit(1);
|
||||
$outputPath = $argv[1] ?? scripts_bundle_output_path();
|
||||
|
||||
try {
|
||||
scripts_stdout(scripts_build_migration_bundle($outputPath));
|
||||
} catch (Throwable $exception) {
|
||||
scripts_stderr($exception->getMessage());
|
||||
}
|
||||
|
||||
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
|
||||
sort($files, SORT_STRING);
|
||||
|
||||
if ($files === []) {
|
||||
fwrite(STDERR, "Keine Migrationsdateien gefunden." . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$outputDir = dirname($outputPath);
|
||||
if (!is_dir($outputDir) && !mkdir($outputDir, 0777, true) && !is_dir($outputDir)) {
|
||||
fwrite(STDERR, "Ausgabeverzeichnis konnte nicht erstellt werden: {$outputDir}" . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$bundle = [];
|
||||
$bundle[] = '-- Generated migration bundle for Kaffeeliste SaaS';
|
||||
$bundle[] = '-- Generated at ' . date('c');
|
||||
$bundle[] = 'SET XACT_ABORT ON;';
|
||||
$bundle[] = 'BEGIN TRANSACTION;';
|
||||
|
||||
foreach ($files as $file) {
|
||||
$sql = require $file;
|
||||
|
||||
if (!is_string($sql) || trim($sql) === '') {
|
||||
fwrite(STDERR, "Ungueltige Migration: {$file}" . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$bundle[] = '';
|
||||
$bundle[] = '-- Migration: ' . basename($file);
|
||||
$bundle[] = trim($sql);
|
||||
}
|
||||
|
||||
$bundle[] = '';
|
||||
$bundle[] = 'COMMIT TRANSACTION;';
|
||||
$bundle[] = '';
|
||||
|
||||
file_put_contents($outputPath, implode(PHP_EOL, $bundle));
|
||||
|
||||
fwrite(STDOUT, $outputPath . PHP_EOL);
|
||||
|
||||
@@ -7,6 +7,7 @@ require_once __DIR__ . '/support.php';
|
||||
$phpVersion = PHP_VERSION;
|
||||
$composerExists = scripts_check_command('composer');
|
||||
$gitExists = scripts_check_command('git');
|
||||
$pdoSqlsrvLoaded = extension_loaded('pdo_sqlsrv');
|
||||
|
||||
scripts_stdout('Pruefe lokale Voraussetzungen fuer Kaffeeliste SaaS...');
|
||||
scripts_stdout('Projektwurzel: ' . scripts_project_root());
|
||||
@@ -16,6 +17,7 @@ $checks = [
|
||||
['PHP', true, $phpVersion],
|
||||
['Composer', $composerExists, $composerExists ? 'verfuegbar' : 'nicht gefunden'],
|
||||
['Git', $gitExists, $gitExists ? 'verfuegbar' : 'nicht gefunden'],
|
||||
['pdo_sqlsrv', $pdoSqlsrvLoaded, $pdoSqlsrvLoaded ? 'geladen' : 'nicht geladen'],
|
||||
['SaaS-App', is_dir(scripts_saas_app_path()), scripts_saas_app_path()],
|
||||
['.env.example', is_file(scripts_env_example_path()), scripts_env_example_path()],
|
||||
['.env', is_file(scripts_env_path()), is_file(scripts_env_path()) ? 'vorhanden' : 'noch nicht angelegt'],
|
||||
|
||||
+14
-15
@@ -6,7 +6,7 @@ require_once __DIR__ . '/support.php';
|
||||
|
||||
$options = scripts_parse_options($argv);
|
||||
$forceEnv = isset($options['force-env']);
|
||||
$prepareEnv = isset($options['prepare-env']) || !is_file(scripts_env_path());
|
||||
$prepareEnv = $forceEnv || isset($options['prepare-env']) || !is_file(scripts_env_path());
|
||||
|
||||
scripts_stdout('Starte SaaS-Installationsvorbereitung...');
|
||||
scripts_stdout('');
|
||||
@@ -14,28 +14,27 @@ scripts_stdout('');
|
||||
require __DIR__ . '/check-prerequisites.php';
|
||||
|
||||
if ($prepareEnv) {
|
||||
$command = 'php ' . escapeshellarg(__DIR__ . '/prepare-saas-env.php');
|
||||
|
||||
if ($forceEnv) {
|
||||
$command .= ' --force';
|
||||
$envValues = scripts_read_env_file(scripts_env_example_path());
|
||||
try {
|
||||
scripts_write_env_file($envValues, scripts_env_path());
|
||||
scripts_stdout('Lokale .env wurde angelegt: ' . scripts_env_path());
|
||||
} catch (Throwable $exception) {
|
||||
scripts_stderr($exception->getMessage());
|
||||
}
|
||||
|
||||
passthru($command, $exitCode);
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
exit($exitCode);
|
||||
if ($forceEnv) {
|
||||
scripts_stdout('Hinweis: vorhandene .env wurde bewusst ueberschrieben.');
|
||||
}
|
||||
}
|
||||
|
||||
$bundlePath = scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . 'all-migrations.sql';
|
||||
$command = 'php ' . escapeshellarg(__DIR__ . '/build-migration-bundle.php') . ' ' . escapeshellarg($bundlePath);
|
||||
passthru($command, $exitCode);
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
exit($exitCode);
|
||||
try {
|
||||
$bundlePath = scripts_build_migration_bundle();
|
||||
} catch (Throwable $exception) {
|
||||
scripts_stderr($exception->getMessage());
|
||||
}
|
||||
|
||||
scripts_stdout('');
|
||||
scripts_stdout('SQL-Bundle erzeugt: ' . $bundlePath);
|
||||
scripts_stdout('Naechste Schritte:');
|
||||
scripts_stdout('1. saas-app/.env mit echten DB-, Mail- und Tenancy-Werten fuellen.');
|
||||
scripts_stdout('2. Das SQL-Bundle manuell importieren oder `php scripts/run-sql-migrations.php --server=... --database=...` verwenden.');
|
||||
|
||||
@@ -7,54 +7,17 @@ require_once __DIR__ . '/support.php';
|
||||
$options = scripts_parse_options($argv);
|
||||
$env = scripts_read_env_file(scripts_env_path());
|
||||
|
||||
$server = $options['server'] ?? $env['DB_HOST'] ?? null;
|
||||
$database = $options['database'] ?? $env['DB_DATABASE'] ?? null;
|
||||
$port = $options['port'] ?? $env['DB_PORT'] ?? '1433';
|
||||
$username = $options['username'] ?? $env['DB_USERNAME'] ?? null;
|
||||
$password = $options['password'] ?? $env['DB_PASSWORD'] ?? null;
|
||||
|
||||
if ($server === null || $database === null) {
|
||||
scripts_stderr('Bitte --server und --database angeben oder eine passende saas-app/.env pflegen.');
|
||||
}
|
||||
|
||||
if (!extension_loaded('pdo_sqlsrv')) {
|
||||
scripts_stderr('Die PHP-Erweiterung pdo_sqlsrv ist nicht geladen. Fuehre das Bundle manuell aus oder aktiviere den Treiber.');
|
||||
}
|
||||
|
||||
$migrationDir = scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
|
||||
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
|
||||
sort($files, SORT_STRING);
|
||||
|
||||
if ($files === []) {
|
||||
scripts_stderr('Keine Migrationsdateien gefunden.');
|
||||
}
|
||||
|
||||
$dsn = sprintf('sqlsrv:Server=%s,%s;Database=%s;TrustServerCertificate=1', $server, $port, $database);
|
||||
$config = [
|
||||
'server' => $options['server'] ?? $env['DB_HOST'] ?? null,
|
||||
'database' => $options['database'] ?? $env['DB_DATABASE'] ?? null,
|
||||
'port' => $options['port'] ?? $env['DB_PORT'] ?? '1433',
|
||||
'username' => $options['username'] ?? $env['DB_USERNAME'] ?? null,
|
||||
'password' => $options['password'] ?? $env['DB_PASSWORD'] ?? null,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $username, $password, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
foreach ($files as $file) {
|
||||
$sql = require $file;
|
||||
|
||||
if (!is_string($sql) || trim($sql) === '') {
|
||||
throw new RuntimeException('Ungueltige Migration: ' . basename($file));
|
||||
}
|
||||
|
||||
scripts_stdout('Fuehre aus: ' . basename($file));
|
||||
$pdo->exec($sql);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
scripts_run_sql_migrations($config);
|
||||
scripts_stdout('Migrationen wurden erfolgreich ausgefuehrt.');
|
||||
} catch (Throwable $exception) {
|
||||
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
scripts_stderr('Migration fehlgeschlagen: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
@@ -22,6 +22,20 @@ function scripts_env_path(): string
|
||||
return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.env';
|
||||
}
|
||||
|
||||
function scripts_bundle_output_path(): string
|
||||
{
|
||||
return scripts_saas_app_path()
|
||||
. DIRECTORY_SEPARATOR . 'database'
|
||||
. DIRECTORY_SEPARATOR . 'migrations'
|
||||
. DIRECTORY_SEPARATOR . 'generated'
|
||||
. DIRECTORY_SEPARATOR . 'all-migrations.sql';
|
||||
}
|
||||
|
||||
function scripts_installer_lock_path(): string
|
||||
{
|
||||
return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.installer.lock';
|
||||
}
|
||||
|
||||
function scripts_check_command(string $command): bool
|
||||
{
|
||||
$where = stripos(PHP_OS_FAMILY, 'Windows') === 0 ? 'where' : 'command -v';
|
||||
@@ -78,6 +92,187 @@ function scripts_read_env_file(string $path): array
|
||||
return $values;
|
||||
}
|
||||
|
||||
function scripts_write_env_file(array $values, ?string $targetPath = null): string
|
||||
{
|
||||
$targetPath ??= scripts_env_path();
|
||||
$templateLines = file(scripts_env_example_path(), FILE_IGNORE_NEW_LINES);
|
||||
|
||||
if ($templateLines === false) {
|
||||
throw new RuntimeException('Die .env.example konnte nicht gelesen werden.');
|
||||
}
|
||||
|
||||
$output = [];
|
||||
|
||||
foreach ($templateLines as $line) {
|
||||
if (!str_contains($line, '=')) {
|
||||
$output[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
[$key] = explode('=', $line, 2);
|
||||
$trimmedKey = trim($key);
|
||||
|
||||
if (!array_key_exists($trimmedKey, $values)) {
|
||||
$output[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
$output[] = $trimmedKey . '=' . scripts_format_env_value((string) $values[$trimmedKey]);
|
||||
unset($values[$trimmedKey]);
|
||||
}
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
$output[] = $key . '=' . scripts_format_env_value((string) $value);
|
||||
}
|
||||
|
||||
if (file_put_contents($targetPath, implode(PHP_EOL, $output) . PHP_EOL) === false) {
|
||||
throw new RuntimeException('Die .env konnte nicht geschrieben werden: ' . $targetPath);
|
||||
}
|
||||
|
||||
return $targetPath;
|
||||
}
|
||||
|
||||
function scripts_format_env_value(string $value): string
|
||||
{
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/\s|#|"|=/', $value) === 1) {
|
||||
return '"' . addcslashes($value, "\\\"") . '"';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function scripts_build_migration_bundle(?string $outputPath = null): string
|
||||
{
|
||||
$projectRoot = scripts_project_root();
|
||||
$migrationDir = $projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
|
||||
$outputPath ??= scripts_bundle_output_path();
|
||||
|
||||
if (!is_dir($migrationDir)) {
|
||||
throw new RuntimeException("Migrationsverzeichnis nicht gefunden: {$migrationDir}");
|
||||
}
|
||||
|
||||
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
|
||||
sort($files, SORT_STRING);
|
||||
|
||||
if ($files === []) {
|
||||
throw new RuntimeException('Keine Migrationsdateien gefunden.');
|
||||
}
|
||||
|
||||
$outputDir = dirname($outputPath);
|
||||
if (!is_dir($outputDir) && !mkdir($outputDir, 0777, true) && !is_dir($outputDir)) {
|
||||
throw new RuntimeException("Ausgabeverzeichnis konnte nicht erstellt werden: {$outputDir}");
|
||||
}
|
||||
|
||||
$bundle = [];
|
||||
$bundle[] = '-- Generated migration bundle for Kaffeeliste SaaS';
|
||||
$bundle[] = '-- Generated at ' . date('c');
|
||||
$bundle[] = 'SET XACT_ABORT ON;';
|
||||
$bundle[] = 'BEGIN TRANSACTION;';
|
||||
|
||||
foreach ($files as $file) {
|
||||
$sql = require $file;
|
||||
|
||||
if (!is_string($sql) || trim($sql) === '') {
|
||||
throw new RuntimeException("Ungueltige Migration: {$file}");
|
||||
}
|
||||
|
||||
$bundle[] = '';
|
||||
$bundle[] = '-- Migration: ' . basename($file);
|
||||
$bundle[] = trim($sql);
|
||||
}
|
||||
|
||||
$bundle[] = '';
|
||||
$bundle[] = 'COMMIT TRANSACTION;';
|
||||
$bundle[] = '';
|
||||
|
||||
if (file_put_contents($outputPath, implode(PHP_EOL, $bundle)) === false) {
|
||||
throw new RuntimeException("Das SQL-Bundle konnte nicht geschrieben werden: {$outputPath}");
|
||||
}
|
||||
|
||||
return $outputPath;
|
||||
}
|
||||
|
||||
function scripts_run_sql_migrations(array $config): array
|
||||
{
|
||||
$server = $config['server'] ?? null;
|
||||
$database = $config['database'] ?? null;
|
||||
$port = (string) ($config['port'] ?? '1433');
|
||||
$username = $config['username'] ?? null;
|
||||
$password = $config['password'] ?? null;
|
||||
|
||||
if ($server === null || $database === null) {
|
||||
throw new RuntimeException('Bitte Server und Datenbank angeben.');
|
||||
}
|
||||
|
||||
if (!extension_loaded('pdo_sqlsrv')) {
|
||||
throw new RuntimeException('Die PHP-Erweiterung pdo_sqlsrv ist nicht geladen.');
|
||||
}
|
||||
|
||||
$migrationDir = scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
|
||||
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
|
||||
sort($files, SORT_STRING);
|
||||
|
||||
if ($files === []) {
|
||||
throw new RuntimeException('Keine Migrationsdateien gefunden.');
|
||||
}
|
||||
|
||||
$dsn = sprintf('sqlsrv:Server=%s,%s;Database=%s;TrustServerCertificate=1', $server, $port, $database);
|
||||
$pdo = new PDO($dsn, $username, $password, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$executed = [];
|
||||
|
||||
try {
|
||||
foreach ($files as $file) {
|
||||
$sql = require $file;
|
||||
|
||||
if (!is_string($sql) || trim($sql) === '') {
|
||||
throw new RuntimeException('Ungueltige Migration: ' . basename($file));
|
||||
}
|
||||
|
||||
$pdo->exec($sql);
|
||||
$executed[] = basename($file);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $executed;
|
||||
}
|
||||
|
||||
function scripts_installer_is_locked(): bool
|
||||
{
|
||||
return is_file(scripts_installer_lock_path());
|
||||
}
|
||||
|
||||
function scripts_installer_lock(array $meta = []): string
|
||||
{
|
||||
$payload = json_encode([
|
||||
'locked_at' => date('c'),
|
||||
'meta' => $meta,
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$path = scripts_installer_lock_path();
|
||||
if (file_put_contents($path, $payload . PHP_EOL) === false) {
|
||||
throw new RuntimeException('Die Installer-Sperrdatei konnte nicht geschrieben werden.');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
function scripts_stdout(string $message): void
|
||||
{
|
||||
fwrite(STDOUT, $message . PHP_EOL);
|
||||
|
||||
Reference in New Issue
Block a user