1.0.3 - php cli install

This commit is contained in:
2026-06-03 10:20:49 +02:00
parent 8404cda475
commit 7381d86362
4 changed files with 679 additions and 551 deletions
+640
View File
@@ -0,0 +1,640 @@
#!/usr/bin/env php
<?php
/**
* Glastree — Installer interattivo per amministratori (PHP CLI)
*
* Uso:
* php install.php
*
* Richiede: PHP 8.3+, Composer, MySQL o SQLite
* Opzionale: npm (per asset frontend)
*
* Modalità:
* 1) Fresh Install su Apache (LAMP)
* 2) Fresh Install via Docker
* 3) Restore da Backup (Apache)
*/
// ── Safety ──────────────────────────────────────────
declare(strict_types=1);
// ── Constants ────────────────────────────────────────
const REQUIREMENTS = ['pdo_mysql', 'mbstring', 'xml', 'curl', 'zip', 'gd', 'fileinfo'];
const MIN_PHP = '8.3.0';
// ── Utilities ────────────────────────────────────────
function println(string $msg = ''): void { echo $msg . PHP_EOL; }
function color(string $text, string $code): string {
$map = [
'red' => '0;31',
'green' => '0;32',
'yellow' => '1;33',
'cyan' => '0;36',
'white' => '1;37',
];
$c = $map[$code] ?? '0';
return "\033[{$c}m{$text}\033[0m";
}
function info(string $msg): void { println(color('[INFO]', 'cyan') . ' ' . $msg); }
function ok(string $msg): void { println(color('[OK]', 'green') . ' ' . $msg); }
function warn(string $msg): void { println(color('[WARN]', 'yellow') . ' ' . $msg); }
function error(string $msg): void { println(color('[ERR]', 'red') . ' ' . $msg); }
function fail(string $msg): never { error($msg); exit(1); }
function ask(string $prompt, ?string $default = null): string {
$defaultStr = $default !== null ? " [{$default}]" : '';
echo color('?', 'cyan') . " {$prompt}{$defaultStr}: ";
$val = trim(fgets(STDIN) ?: '');
return $val !== '' ? $val : ($default ?? '');
}
function askRequired(string $prompt): string {
while (true) {
$val = ask($prompt);
if ($val !== '') break;
warn('Valore obbligatorio.');
}
return $val;
}
function confirm(string $prompt): bool {
$resp = strtolower(ask($prompt . ' [S/n]', 'S'));
return $resp === 's' || $resp === '';
}
function menu(string $prompt, array $options): string {
foreach ($options as $i => $opt) {
println(' ' . ($i + 1) . ') ' . $opt);
}
while (true) {
$val = ask($prompt, '1');
if (isset($options[(int)$val - 1])) return $options[(int)$val - 1];
warn('Scelta non valida.');
}
}
function checkCmd(string $cmd): bool {
$found = !(trim(shell_exec("command -v $cmd 2>/dev/null") ?: '') === '');
if (!$found) warn("Comando '$cmd' non trovato.");
return $found;
}
function run(string $cmd, ?string $label = null): bool {
if ($label) info($label);
println(" \$ {$cmd}");
passthru($cmd, $exitCode);
if ($exitCode !== 0) warn("Comando terminato con codice {$exitCode}");
return $exitCode === 0;
}
function runCapture(string $cmd): string {
return trim(shell_exec($cmd) ?: '');
}
// ── Banner ───────────────────────────────────────────
println(color(' ____ _ _ _', 'white'));
println(color(' / ___| | __ _ ___| |_ ___ _ __ ___ | |_ ___', 'white'));
println(color('| | _| |/ _` / __| __/ _ \'__/ _ \ | __/ _ \\', 'white'));
println(color('| |_| | | (_| \__ \ || __/ | | __/ | || __/', 'white'));
println(color(' \____|_|\__,_|___/\__\___|_| \___| \__\___|', 'white'));
println(color(' Installer interattivo per sysadmin', 'cyan'));
println();
// ── Preflight ────────────────────────────────────────
$scriptDir = dirname(__FILE__);
chdir($scriptDir);
if (!file_exists('.env.example')) {
fail("File .env.example non trovato in {$scriptDir}. Sei nella cartella dell'applicazione?");
}
// ── Select mode ──────────────────────────────────────
println('Seleziona la modalità di installazione:');
$mode = menu('Scegli', [
'Fresh Install su Apache (LAMP tradizionale)',
'Fresh Install via Docker',
'Restore da Backup (Apache)',
]);
// ── Common params ────────────────────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' PARAMETRI GENERALI', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
$appName = askRequired('Nome del sito/app (es. Glastree MyChurch)');
$adminName = askRequired('Nome del primo utente amministratore');
$adminEmail = askRequired('Email dell\'amministratore');
$adminPass = askRequired('Password per l\'amministratore');
$appUrl = askRequired('URL pubblico del sito (es. https://glastree.esempio.it)');
// ── DB params (Apache / Restore) ─────────────────────
$dbType = 'mysql';
$dbHost = '127.0.0.1';
$dbPort = '3306';
$dbName = 'glastree';
$dbUser = 'glastree';
$dbPass = '';
if (in_array($mode, ['Fresh Install su Apache (LAMP tradizionale)', 'Restore da Backup (Apache)'])) {
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' DATABASE', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
$dbType = menu('Tipo database', ['mysql', 'sqlite']);
if ($dbType === 'mysql') {
$dbHost = askRequired('Host database');
$dbPort = ask('Porta database', '3306');
$dbName = ask('Nome database (verrà creato se non esiste)', 'glastree');
$dbUser = askRequired('Utente database');
$dbPass = askRequired('Password database');
}
}
// ══════════════════════════════════════════════════════
// MODE 1: FRESH INSTALL APACHE
// ══════════════════════════════════════════════════════
if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') {
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' PRE-FLIGHT CHECKS', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
// PHP version
$phpVer = PHP_VERSION;
info("PHP versione: {$phpVer}");
if (version_compare($phpVer, MIN_PHP, '<')) {
fail("PHP >= " . MIN_PHP . " richiesto (trovato {$phpVer})");
}
ok("PHP {$phpVer}");
// Extensions
foreach (REQUIREMENTS as $ext) {
if (!extension_loaded($ext)) {
warn("Estensione PHP '{$ext}' non trovata (alcune funzionalità potrebbero non funzionare)");
} else {
ok("Estensione PHP '{$ext}'");
}
}
// Composer
checkCmd('composer');
checkCmd('node');
checkCmd('npm');
// ── Step 1: Environment ─────────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' 1/7 — CONFIGURAZIONE AMBIENTE', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
if (!file_exists('.env')) {
copy('.env.example', '.env');
ok('.env creato da .env.example');
} else {
info('.env già esistente, lo aggiorno');
}
// APP_KEY
runCapture("php artisan key:generate --force");
ok('APP_KEY generata');
// Config
$dotenv = file_get_contents('.env');
$replacements = [
'/^APP_NAME=.*/m' => 'APP_NAME="' . addslashes($appName) . '"',
'/^APP_URL=.*/m' => "APP_URL={$appUrl}",
'/^APP_ENV=.*/m' => 'APP_ENV=production',
'/^APP_DEBUG=.*/m' => 'APP_DEBUG=false',
];
if ($dbType === 'mysql') {
$replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=mysql';
$replacements['/^DB_HOST=.*/m'] = "DB_HOST={$dbHost}";
$replacements['/^DB_PORT=.*/m'] = "DB_PORT={$dbPort}";
$replacements['/^DB_DATABASE=.*/m'] = "DB_DATABASE={$dbName}";
$replacements['/^DB_USERNAME=.*/m'] = "DB_USERNAME={$dbUser}";
$replacements['/^DB_PASSWORD=.*/m'] = "DB_PASSWORD={$dbPass}";
} else {
$replacements['/^DB_CONNECTION=.*/m'] = 'DB_CONNECTION=sqlite';
$replacements['/^DB_HOST=.*/m'] = '# DB_HOST=';
$replacements['/^DB_PORT=.*/m'] = '# DB_PORT=';
$replacements['/^DB_DATABASE=.*/m'] = 'DB_DATABASE=' . $scriptDir . '/database/database.sqlite';
$replacements['/^DB_USERNAME=.*/m'] = '# DB_USERNAME=';
$replacements['/^DB_PASSWORD=.*/m'] = '# DB_PASSWORD=';
}
foreach ($replacements as $pattern => $replacement) {
$dotenv = preg_replace($pattern, $replacement, $dotenv);
}
file_put_contents('.env', $dotenv);
ok('.env configurato');
// ── Step 2: Database ────────────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' 2/7 — DATABASE', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
if ($dbType === 'mysql') {
info("Connessione a MySQL {$dbHost}:{$dbPort}...");
$connected = false;
for ($i = 0; $i < 30; $i++) {
try {
new PDO("mysql:host={$dbHost};port={$dbPort}", $dbUser, $dbPass);
$connected = true;
break;
} catch (\PDOException) {
sleep(2);
}
}
if ($connected) {
ok('MySQL raggiungibile');
} else {
warn('Timeout MySQL — verificare che il servizio sia attivo');
}
info("Creazione database '{$dbName}'...");
try {
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort}", $dbUser, $dbPass);
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
ok("Database '{$dbName}' pronto");
} catch (\PDOException $e) {
warn("Crea manualmente: mysql -u root -p -e \"CREATE DATABASE `{$dbName}` CHARACTER SET utf8mb4;\"");
}
} else {
$sqlitePath = $scriptDir . '/database/database.sqlite';
if (!file_exists($sqlitePath)) {
touch($sqlitePath);
chmod($sqlitePath, 0664);
}
ok('SQLite pronto: ' . $sqlitePath);
}
// ── Step 3: Composer ────────────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' 3/7 — INSTALLAZIONE DIPENDENZE', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
run('composer install --no-dev --optimize-autoloader', 'Installazione dipendenze Composer...');
ok('Dipendenze PHP installate');
// ── Step 4: Package discovery ────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' 4/7 — REGISTRAZIONE PACCHETTI', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
runCapture("php artisan package:discover --ansi 2>/dev/null");
ok('Pacchetti registrati');
// ── Step 5: Migration + Seed ─────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' 5/7 — MIGRATION E SEED', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
if (!run('php artisan migrate --seed --force', 'Esecuzione migration e seed...')) {
fail('Migration fallita. Verifica le credenziali del database.');
}
ok('Struttura database creata e dati di base inseriti');
runCapture('php artisan storage:link --force 2>/dev/null');
// ── Step 6: Admin user ───────────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' 6/7 — CREAZIONE AMMINISTRATORE', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
$tinkerScript = <<<PHP
\App\Models\User::where('email', '{$adminEmail}')->delete();
\$u = new \App\Models\User;
\$u->name = '{$adminName}';
\$u->email = '{$adminEmail}';
\$u->password = bcrypt('{$adminPass}');
\$u->save();
echo "OK: " . \$u->id;
PHP;
$result = runCapture("php artisan tinker --execute=" . escapeshellarg($tinkerScript) . " 2>/dev/null");
if (str_contains($result, 'OK:')) {
ok("Amministratore {$adminEmail} creato");
} else {
warn("Creazione admin fallita — crealo manualmente dopo l'installazione");
}
// ── Step 7: Asset ────────────────────────────────
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' 7/7 — ASSET FRONTEND', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
$hasNpm = checkCmd('npm');
if ($hasNpm && confirm('Compilare gli asset frontend con npm?')) {
run('npm install', 'Installazione dipendenze npm...');
run('npm run build', 'Compilazione asset...');
ok('Asset compilati');
} else {
if (!$hasNpm) warn('npm non disponibile — asset non compilati (AdminLTE via CDN)');
else info('Asset non compilati (AdminLTE via CDN)');
}
// ── Permissions ──────────────────────────────────
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
info('Impostazione permessi...');
$wwwUser = trim(shell_exec('ps aux | grep -E "apache|httpd" | grep -v grep | head -1 | awk \'{print \$1}\'') ?: 'www-data');
runCapture("chown -R {$wwwUser}:{$wwwUser} storage bootstrap/cache 2>/dev/null");
chmod("{$scriptDir}/storage", 0775);
chmod("{$scriptDir}/bootstrap/cache", 0775);
ok("Permessi impostati (utente: {$wwwUser})");
}
// ── Summary ──────────────────────────────────────
println();
println(color('══════════════════════════════════════════', 'green'));
println(color(' INSTALLAZIONE COMPLETATA!', 'green'));
println(color('══════════════════════════════════════════', 'green'));
println();
println(" URL: {$appUrl}");
println(" Admin: {$adminEmail}");
println(" Cartella: {$scriptDir}");
println();
warn('Configura Apache con il VirtualHost (vedi Guida → Installazione, Passo 4).');
if ($dbType === 'mysql') {
warn('Se hai appena creato il database, assicurati che l\'utente abbia privilegi completi.');
}
}
// ══════════════════════════════════════════════════════
// MODE 2: DOCKER
// ══════════════════════════════════════════════════════
if ($mode === 'Fresh Install via Docker') {
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' PRE-FLIGHT CHECKS (Docker)', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
checkCmd('docker');
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' CONFIGURAZIONE DOCKER', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
$dockerDb = menu('Tipo database per Docker', ['sqlite', 'mysql']);
$dockerPort = ask('Porta host (es. 8080)', '8080');
$composeFile = $dockerDb === 'mysql' ? 'docker-compose.mysql.yml' : 'docker-compose.yml';
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' INSTALLAZIONE DOCKER', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
if (!run("docker compose -f {$composeFile} build", 'Build immagine Docker...')) {
fail('Build fallita');
}
// Override compose con variabili d'ambiente
$override = [
'services:',
' glastree:',
' environment:',
" - APP_NAME={$appName}",
" - APP_URL={$appUrl}",
' - APP_ENV=production',
' - APP_DEBUG=false',
];
file_put_contents('docker-compose.override.yml', implode(PHP_EOL, $override) . PHP_EOL);
if (!run(
"docker compose -f {$composeFile} -f docker-compose.override.yml up -d",
'Avvio container...'
)) {
@unlink('docker-compose.override.yml');
fail('Avvio container fallito');
}
// Attendi container pronto
info('Attendo che il container sia pronto...');
$containerId = runCapture("docker compose -f {$composeFile} ps -q glastree 2>/dev/null");
$adminCreated = false;
if ($containerId !== '') {
for ($i = 0; $i < 30; $i++) {
$status = runCapture("docker inspect -f '{{.State.Status}}' {$containerId} 2>/dev/null");
if ($status === 'running') {
$http = runCapture("docker exec {$containerId} sh -c 'wget -q -O- http://localhost/ 2>/dev/null && echo OK'");
if (str_contains($http, 'OK')) {
info('Container pronto');
break;
}
}
sleep(2);
}
$tinkerScript = <<<PHP
\App\Models\User::where('email', '{$adminEmail}')->delete();
\$u = new \App\Models\User;
\$u->name = '{$adminName}';
\$u->email = '{$adminEmail}';
\$u->password = bcrypt('{$adminPass}');
\$u->save();
echo "OK: " . \$u->id;
PHP;
$result = runCapture(
"docker exec -i {$containerId} php artisan tinker --execute=" . escapeshellarg($tinkerScript) . " 2>/dev/null"
);
if (str_contains($result, 'OK:')) {
ok("Amministratore {$adminEmail} creato nel container");
$adminCreated = true;
} else {
warn("Creazione admin fallita — crealo manualmente dopo l'installazione");
}
}
@unlink('docker-compose.override.yml');
println();
println(color('══════════════════════════════════════════', 'green'));
println(color(' INSTALLAZIONE DOCKER COMPLETATA!', 'green'));
println(color('══════════════════════════════════════════', 'green'));
println();
println(" URL: http://localhost:{$dockerPort}");
println(" Admin: {$adminEmail}");
println(" Container: {$containerId}");
println();
warn('Per URL pubblici, imposta APP_URL nelle environment del container.');
warn('Dati persistenti: volumi Docker (glastree_storage, glastree_db o mysql_data).');
}
// ══════════════════════════════════════════════════════
// MODE 3: RESTORE DA BACKUP
// ══════════════════════════════════════════════════════
if ($mode === 'Restore da Backup (Apache)') {
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' PRE-FLIGHT CHECKS (Restore)', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
checkCmd('php');
checkCmd('composer');
checkCmd('unzip');
if ($dbType === 'mysql') {
checkCmd('mysql');
}
$backupZip = askRequired('Percorso del file ZIP di backup');
if (!file_exists($backupZip)) {
fail("File non trovato: {$backupZip}");
}
println();
println(color('══════════════════════════════════════════', 'yellow'));
println(color(' RESTORE DA BACKUP', 'yellow'));
println(color('══════════════════════════════════════════', 'yellow'));
// Extract
$tmpDir = sys_get_temp_dir() . '/glastree-restore-' . uniqid();
mkdir($tmpDir, 0700, true);
info("Estrazione backup in {$tmpDir}...");
if (!run("unzip -qo {$backupZip} -d {$tmpDir}", null)) {
// pulisci tmp dir prima di fallire
runCapture("rm -rf {$tmpDir}");
fail('Estrazione backup fallita');
}
// Environment
if (!file_exists('.env')) {
copy('.env.example', '.env');
}
// Restore .env from backup preserving DB credentials
if (file_exists("{$tmpDir}/.env")) {
$currentEnv = file_get_contents('.env');
$backupEnv = file_get_contents("{$tmpDir}/.env");
// Save current DB_* values
$currentDb = [];
foreach (['DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD'] as $key) {
if (preg_match("/^{$key}=(.*)$/m", $currentEnv, $m)) {
$currentDb[$key] = $m[1];
}
}
// Use backup .env as base
file_put_contents('.env', $backupEnv);
// Restore current DB credentials
foreach ($currentDb as $key => $val) {
$dotenv = file_get_contents('.env');
$dotenv = preg_replace("/^{$key}=.*/m", "{$key}={$val}", $dotenv);
file_put_contents('.env', $dotenv);
}
ok('.env ripristinato dal backup (credenziali DB preservate)');
}
// Update APP_* config
$dotenv = file_get_contents('.env');
$dotenv = preg_replace('/^APP_NAME=.*/m', 'APP_NAME="' . addslashes($appName) . '"', $dotenv);
$dotenv = preg_replace('/^APP_URL=.*/m', "APP_URL={$appUrl}", $dotenv);
$dotenv = preg_replace('/^APP_ENV=.*/m', 'APP_ENV=production', $dotenv);
$dotenv = preg_replace('/^APP_DEBUG=.*/m', 'APP_DEBUG=false', $dotenv);
file_put_contents('.env', $dotenv);
// Generate APP_KEY
runCapture('php artisan key:generate --force');
ok('APP_KEY rigenerata');
// Import database
if ($dbType === 'mysql') {
$sqlFile = "{$tmpDir}/database.sql";
if (file_exists($sqlFile)) {
info("Importazione database MySQL: {$sqlFile}");
$cmd = "mysql -h {$dbHost} -P {$dbPort} -u {$dbUser} -p{$dbPass} {$dbName} < {$sqlFile}";
if (run($cmd, null)) {
ok('Database importato');
} else {
warn('Import database fallito — importa manualmente');
}
} else {
warn('database.sql non trovato nel backup');
}
} else {
warn('Restore automatico supportato solo con MySQL. Per SQLite ripristina manualmente i file.');
}
// Restore storage
$storageBackup = "{$tmpDir}/storage";
if (is_dir($storageBackup)) {
info('Ripristino file storage...');
runCapture("cp -r {$storageBackup}/* {$scriptDir}/storage/ 2>/dev/null");
ok('Storage ripristinato');
}
// Composer
run('composer install --no-dev --optimize-autoloader', 'Installazione dipendenze Composer...');
runCapture('php artisan package:discover --ansi 2>/dev/null');
// Storage link + cache
runCapture('php artisan storage:link --force 2>/dev/null');
runCapture('php artisan optimize:clear');
// Permissions
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
$wwwUser = trim(shell_exec('ps aux | grep -E "apache|httpd" | grep -v grep | head -1 | awk \'{print \$1}\'') ?: 'www-data');
runCapture("chown -R {$wwwUser}:{$wwwUser} storage bootstrap/cache 2>/dev/null");
chmod("{$scriptDir}/storage", 0775);
chmod("{$scriptDir}/bootstrap/cache", 0775);
ok("Permessi impostati (utente: {$wwwUser})");
}
// Create admin if not exists
$tinkerScript = <<<PHP
\$exists = \App\Models\User::where('email', '{$adminEmail}')->exists();
if (!\$exists) {
\$u = new \App\Models\User;
\$u->name = '{$adminName}';
\$u->email = '{$adminEmail}';
\$u->password = bcrypt('{$adminPass}');
\$u->save();
echo 'OK';
} else {
echo 'EXISTS';
}
PHP;
$result = runCapture("php artisan tinker --execute=" . escapeshellarg($tinkerScript) . " 2>/dev/null");
if (str_contains($result, 'OK')) {
ok("Amministratore {$adminEmail} creato");
} elseif (str_contains($result, 'EXISTS')) {
ok("Amministratore {$adminEmail} già esistente");
}
// Cleanup
runCapture("rm -rf {$tmpDir}");
println();
println(color('══════════════════════════════════════════', 'green'));
println(color(' RESTORE COMPLETATO!', 'green'));
println(color('══════════════════════════════════════════', 'green'));
println();
println(" URL: {$appUrl}");
println(" Admin: {$adminEmail}");
println(" Cartella: {$scriptDir}");
println();
warn('Verifica che APP_KEY non abbia invalidato dati criptati (token OAuth, password IMAP).');
warn('Se necessario, riconfigura SMTP e Google Drive dalle Impostazioni.');
}
println();
-430
View File
@@ -1,430 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# ─────────────────────────────────────────────────────
# Glastree — Installer interattivo per amministratori
# Richiede: bash 4+, curl, git (opzionale), composer,
# php 8.3+, node 20+, npm 10+, mysql (opz.)
# ─────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERR]${NC} $*"; }
fail() { error "$*"; exit 1; }
ask() {
local prompt="$1" var_name="$2" default="${3:-}"
local val
if [ -n "$default" ]; then
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [${default}]: ")" val
val="${val:-$default}"
else
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" val
fi
printf -v "$var_name" '%s' "$val"
}
ask_required() {
local prompt="$1" var_name="$2"
local val
while true; do
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" val
[ -n "$val" ] && break
warn "Valore obbligatorio."
done
printf -v "$var_name" '%s' "$val"
}
confirm() {
local prompt="$1"
local resp
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [S/n]: ")" resp
[[ "$resp" =~ ^[Ss]?$ ]]
}
check_cmds() {
local missing=()
for c in "$@"; do
command -v "$c" &>/dev/null || missing+=("$c")
done
[ ${#missing[@]} -gt 0 ] && fail "Comandi mancanti: ${missing[*]}"
}
print_banner() {
cat << 'EOF'
____ _ _ _
/ ___| | __ _ ___| |_ ___ _ __ ___ | |_ ___
| | _| |/ _` / __| __/ _ \ '__/ _ \ | __/ _ \
| |_| | | (_| \__ \ || __/ | | __/ | || __/
\____|_|\__,_|___/\__\___|_| \___| \__\___|
Installer interattivo per sysadmin
EOF
}
# ─────────────────────────────────────────────────────
# PREFLIGHT
# ─────────────────────────────────────────────────────
print_banner
# Directory dello script
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# Rileva modalità
echo ""
echo "Seleziona la modalità di installazione:"
echo " 1) Fresh Install su Apache (LAMP tradizionale)"
echo " 2) Fresh Install via Docker"
echo " 3) Restore da Backup (Apache)"
ask "Scegli [1/2/3]" INSTALL_MODE "1"
case "$INSTALL_MODE" in
1) MODE="apache" ;;
2) MODE="docker" ;;
3) MODE="restore" ;;
*) fail "Scelta non valida: $INSTALL_MODE" ;;
esac
# ─────────────────────────────────────────────────────
# PARAMETRI COMUNI
# ─────────────────────────────────────────────────────
echo ""
echo "══════════════════════════════════════════"
echo " PARAMETRI GENERALI"
echo "══════════════════════════════════════════"
ask_required "Nome del sito/app (es. Glastree MyChurch)" APP_NAME
ask_required "Nome del primo utente amministratore" ADMIN_NAME
ask_required "Email dell'amministratore" ADMIN_EMAIL
ask_required "Password per l'amministratore" ADMIN_PASSWORD
ask_required "URL pubblico del sito (es. https://glastree.esempio.it)" APP_URL
# ─────────────────────────────────────────────────────
# APACHE
# ─────────────────────────────────────────────────────
if [ "$MODE" = "apache" ] || [ "$MODE" = "restore" ]; then
echo ""
echo "══════════════════════════════════════════"
echo " DATABASE"
echo "══════════════════════════════════════════"
ask "Tipo database" DB_TYPE "mysql" # mysql|sqlite
if [ "$DB_TYPE" = "mysql" ]; then
ask_required "Host database" DB_HOST
ask "Porta database" DB_PORT "3306"
ask_required "Nome database (verrà creato se non esiste)" DB_NAME
ask_required "Utente database" DB_USER
ask_required "Password database" DB_PASS
fi
fi
# ─────────────────────────────────────────────────────
# APACHE — FRESH INSTALL
# ─────────────────────────────────────────────────────
if [ "$MODE" = "apache" ]; then
echo ""
echo "══════════════════════════════════════════"
echo " PRE-FLIGHT CHECKS (Apache)"
echo "══════════════════════════════════════════"
check_cmds php composer
command -v node &>/dev/null || warn "node non trovato — asset frontend non compilabili (usa CDN)"
command -v npm &>/dev/null || warn "npm non trovato — asset frontend non compilabili (usa CDN)"
PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
info "PHP versione: $PHP_VER"
if php -r 'exit(version_compare(PHP_VERSION, "8.3.0") ? 0 : 1);' 2>/dev/null; then
fail "PHP >= 8.3 richiesto (trovato $PHP_VER)"
fi
for ext in pdo_mysql mbstring xml curl zip gd fileinfo; do
php -m | grep -qi "$ext" || warn "Estensione PHP '$ext' non trovata"
done
echo ""
echo "══════════════════════════════════════════"
echo " INSTALLAZIONE APACHE"
echo "══════════════════════════════════════════"
info "Percorso installazione: $SCRIPT_DIR"
# 1. Ambiente
[ ! -f .env ] && cp .env.example .env
"$SCRIPT_DIR"/artisan key:generate --force
# 2. Config .env
sed -i "s|APP_NAME=.*|APP_NAME=\"$APP_NAME\"|" .env
sed -i "s|APP_URL=.*|APP_URL=$APP_URL|" .env
sed -i "s|APP_ENV=.*|APP_ENV=production|" .env
sed -i "s|APP_DEBUG=.*|APP_DEBUG=false|" .env
if [ "$DB_TYPE" = "mysql" ]; then
sed -i "s/DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env
sed -i "s/DB_HOST=.*/DB_HOST=$DB_HOST/" .env
sed -i "s/DB_PORT=.*/DB_PORT=$DB_PORT/" .env
sed -i "s/DB_DATABASE=.*/DB_DATABASE=$DB_NAME/" .env
sed -i "s/DB_USERNAME=.*/DB_USERNAME=$DB_USER/" .env
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$DB_PASS/" .env
info "Attendo MySQL..."
php -r "
\$start = time();
while (time() - \$start < 60) {
try {
new PDO('mysql:host=$DB_HOST;port=$DB_PORT', '$DB_USER', '$DB_PASS');
exit(0);
} catch (\PDOException \$e) {
sleep(2);
}
}
exit(1);
" && ok "MySQL pronto" || warn "Timeout MySQL — continuo comunque"
php "$SCRIPT_DIR"/artisan config:clear 2>/dev/null || true
else
sed -i "s/DB_CONNECTION=.*/DB_CONNECTION=sqlite/" .env
touch database/database.sqlite
chmod 664 database/database.sqlite
fi
# 3. Composer
info "Installazione dipendenze Composer..."
composer install --no-dev --optimize-autoloader --no-scripts
# 4. Migration + seed
info "Esecuzione migration e seed..."
php "$SCRIPT_DIR"/artisan migrate --seed --force
# 5. Storage link
php "$SCRIPT_DIR"/artisan storage:link --force 2>/dev/null || true
# 6. Crea admin
info "Creazione utente amministratore..."
php "$SCRIPT_DIR"/artisan tinker --execute="
\$u = new \App\Models\User;
\$u->name = '$ADMIN_NAME';
\$u->email = '$ADMIN_EMAIL';
\$u->password = bcrypt('$ADMIN_PASSWORD');
\$u->save();
" 2>/dev/null
# 7. Asset
if command -v npm &>/dev/null; then
info "Compilazione asset frontend..."
npm ci --ignore-scripts 2>/dev/null && npm run build
else
warn "npm non trovato — asset non compilati (usa AdminLTE via CDN)"
fi
# 8. Permessi
chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || warn "chown fallito — esegui manualmente: chown -R www-data:www-data storage bootstrap/cache"
chmod -R 775 storage bootstrap/cache
ok "Installazione Apache completata!"
echo ""
echo "────────────────────────────────────────────"
echo " URL: $APP_URL"
echo " Admin: $ADMIN_EMAIL"
echo " Cartella: $SCRIPT_DIR"
echo "────────────────────────────────────────────"
echo ""
warn "Configura Apache con il VirtualHost che trovi nella Guida → Installazione."
warn "Se hai appena creato il database MySQL, assicurati che l'utente abbia privilegi CREATE."
fi
# ─────────────────────────────────────────────────────
# DOCKER
# ─────────────────────────────────────────────────────
if [ "$MODE" = "docker" ]; then
echo ""
echo "══════════════════════════════════════════"
echo " PRE-FLIGHT CHECKS (Docker)"
echo "══════════════════════════════════════════"
check_cmds docker
echo ""
echo "══════════════════════════════════════════"
echo " CONFIGURAZIONE DOCKER"
echo "══════════════════════════════════════════"
ask "Tipo database per Docker" DOCKER_DB "sqlite"
ask "Porta host (es. 8080)" DOCKER_PORT "8080"
COMPOSE_FILE="docker-compose.yml"
[ "$DOCKER_DB" = "mysql" ] && COMPOSE_FILE="docker-compose.mysql.yml"
echo ""
echo "══════════════════════════════════════════"
echo " INSTALLAZIONE DOCKER"
echo "══════════════════════════════════════════"
info "Build immagine Docker..."
docker compose -f "$COMPOSE_FILE" build
# Crea un .env temporaneo per il docker-entrypoint
# Il container genera APP_KEY e fa migrate da solo
# Ma dobbiamo passare le variabili per admin e app name
cat > docker-compose.override.yml << EOF
services:
glastree:
environment:
- APP_NAME=$APP_NAME
- APP_URL=$APP_URL
- APP_ENV=production
- APP_DEBUG=false
EOF
# Se mysql, la configurazione DB è già nel compose
# Se sqlite, non serve override per DB
info "Avvio container..."
docker compose -f "$COMPOSE_FILE" -f docker-compose.override.yml up -d
echo ""
info "Attendo che il container sia pronto..."
CONTAINER_NAME=$(docker compose -f "$COMPOSE_FILE" ps -q glastree 2>/dev/null | head -1)
if [ -n "$CONTAINER_NAME" ]; then
# Attendi che Apache sia in ascolto
for i in $(seq 1 30); do
docker exec "$CONTAINER_NAME" curl -sf -o /dev/null http://localhost/ 2>/dev/null && break
sleep 2
done
# Crea utente admin dentro il container
info "Creazione utente amministratore nel container..."
docker exec -i "$CONTAINER_NAME" php artisan tinker --execute="
\App\Models\User::where('email', '$ADMIN_EMAIL')->delete();
\$u = new \App\Models\User;
\$u->name = '$ADMIN_NAME';
\$u->email = '$ADMIN_EMAIL';
\$u->password = bcrypt('$ADMIN_PASSWORD');
\$u->save();
" 2>/dev/null
fi
rm -f docker-compose.override.yml
ok "Installazione Docker completata!"
echo ""
echo "────────────────────────────────────────────"
echo " URL: http://localhost:${DOCKER_PORT}"
echo " Admin: $ADMIN_EMAIL"
echo " Container: $(docker compose -f "$COMPOSE_FILE" ps -q glastree 2>/dev/null | head -1)"
echo "────────────────────────────────────────────"
echo ""
warn "Per URL pubblici, imposta APP_URL nelle environment del container."
warn "Dati persistenti: volume 'glastree_storage' e 'glastree_db' (sqlite) o 'mysql_data'."
fi
# ─────────────────────────────────────────────────────
# RESTORE DA BACKUP (Apache)
# ─────────────────────────────────────────────────────
if [ "$MODE" = "restore" ]; then
echo ""
echo "══════════════════════════════════════════"
echo " PRE-FLIGHT CHECKS (Restore)"
echo "══════════════════════════════════════════"
check_cmds php composer unzip mysql
ask_required "Percorso del file ZIP di backup" BACKUP_ZIP
[ ! -f "$BACKUP_ZIP" ] && fail "File non trovato: $BACKUP_ZIP"
echo ""
echo "══════════════════════════════════════════"
echo " RESTORE DA BACKUP"
echo "══════════════════════════════════════════"
# Estrai backup in temp
TMP_DIR=$(mktemp -d)
info "Estrazione backup in $TMP_DIR ..."
unzip -q "$BACKUP_ZIP" -d "$TMP_DIR"
# 1. Ambiente
[ ! -f .env ] && cp .env.example .env
# 2. Ripristina .env dal backup (preservando DB creds)
if [ -f "$TMP_DIR/.env" ]; then
cp .env .env.backup-pre-restore
# Estrai DB_* dal .env corrente
for key in DB_CONNECTION DB_HOST DB_PORT DB_DATABASE DB_USERNAME DB_PASSWORD; do
current_val=$(grep "^${key}=" .env 2>/dev/null | cut -d= -f2-)
[ -n "$current_val" ] && export "CURRENT_${key}=$current_val"
done
cp "$TMP_DIR/.env" .env
# Ripristina le credenziali DB del nuovo server
for key in DB_CONNECTION DB_HOST DB_PORT DB_DATABASE DB_USERNAME DB_PASSWORD; do
var="CURRENT_${key}"
if [ -n "${!var:-}" ]; then
sed -i "s|^${key}=.*|${key}=${!var}|" .env
fi
done
ok ".env ripristinato dal backup (credenziali DB preservate)"
fi
sed -i "s|APP_NAME=.*|APP_NAME=\"$APP_NAME\"|" .env
sed -i "s|APP_URL=.*|APP_URL=$APP_URL|" .env
sed -i "s|APP_ENV=.*|APP_ENV=production|" .env
sed -i "s|APP_DEBUG=.*|APP_DEBUG=false|" .env
# 3. rigenera APP_KEY
"$SCRIPT_DIR"/artisan key:generate --force
# 4. Importa database
if [ "$DB_TYPE" = "mysql" ]; then
info "Importazione database MySQL..."
if [ -f "$TMP_DIR/database.sql" ]; then
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$TMP_DIR/database.sql"
ok "Database importato"
else
warn "database.sql non trovato nel backup"
fi
else
warn "Tipo database non supportato per restore automatico (usa SQLite o importa manualmente)"
fi
# 5. Ripristina storage
if [ -d "$TMP_DIR/storage" ]; then
info "Ripristino file storage..."
cp -r "$TMP_DIR/storage"/* "$SCRIPT_DIR"/storage/ 2>/dev/null || true
ok "Storage ripristinato"
fi
# 6. Composer
composer install --no-dev --optimize-autoloader --no-scripts
# 7. Storage link + cache
php "$SCRIPT_DIR"/artisan storage:link --force 2>/dev/null || true
php "$SCRIPT_DIR"/artisan optimize:clear
# 8. Permessi
chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || warn "chown fallito"
chmod -R 775 storage bootstrap/cache
# 9. Crea admin se non esiste
php "$SCRIPT_DIR"/artisan tinker --execute="
\$exists = \App\Models\User::where('email', '$ADMIN_EMAIL')->exists();
if (!\$exists) {
\$u = new \App\Models\User;
\$u->name = '$ADMIN_NAME';
\$u->email = '$ADMIN_EMAIL';
\$u->password = bcrypt('$ADMIN_PASSWORD');
\$u->save();
}
" 2>/dev/null
# Cleanup
rm -rf "$TMP_DIR"
ok "Restore completato!"
echo ""
echo "────────────────────────────────────────────"
echo " URL: $APP_URL"
echo " Admin: $ADMIN_EMAIL"
echo " Cartella: $SCRIPT_DIR"
echo "────────────────────────────────────────────"
echo ""
warn "Verifica che la APP_KEY non abbia invalidato dati criptati (token OAuth, password IMAP)."
warn "Se necessario, riconfigura SMTP e Google Drive dalle Impostazioni."
fi
+30 -64
View File
@@ -775,7 +775,7 @@ echo 'Utente creato con successo!';
</div>
<div class="card-body">
<h5>Panoramica</h5>
<p>L'installazione avviene tramite comandi <strong>Artisan</strong> da terminale. Non esiste un wizard web interattivo. Supporta due modalita:</p>
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalita:</p>
<div class="row">
<div class="col-md-6">
<div class="callout callout-info">
@@ -829,19 +829,37 @@ git clone &lt;URL_REPOSITORY&gt; glastree
# Opzione B — ZIP (se non hai git)
# unzip /percorso/del/glastree.zip -d glastree
cd glastree
composer install --no-dev --optimize-autoloader
npm ci && npm run build</code></pre>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN.
cd glastree</code></pre>
<h6>Passo 3: Eseguire l'installer</h6>
<p>Lancerai l'installer interattivo che ti guidera attraverso tutte le fasi:</p>
<pre><code>php install.php</code></pre>
<p>L'installer ti chiedera:</p>
<ol>
<li><strong>Modalità</strong>: Fresh Install (Apache/Docker) o Restore da Backup</li>
<li><strong>Parametri generali</strong>: nome sito, amministratore (nome, email, password), URL pubblico</li>
<li><strong>Database</strong>: MySQL (host, porta, nome, utente, password) o SQLite</li>
</ol>
<p>L'installer esegue automaticamente:</p>
<ul>
<li>Generazione <code>.env</code> e <code>APP_KEY</code></li>
<li>Creazione del database</li>
<li><code>composer install</code> e registrazione pacchetti</li>
<li>Migration e seed dei dati di base</li>
<li>Creazione dell'utente amministratore</li>
<li>Compilazione asset frontend (se npm disponibile)</li>
<li>Impostazione permessi cartelle</li>
</ul>
<div class="callout callout-info">
<h6><i class="fas fa-info-circle"></i> Docker</h6>
<p>Se scegli la modalita Docker, l'installer builda l'immagine, avvia i container e crea l'admin all'interno del container.</p>
</div>
<div class="callout callout-success">
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
<p>Se scegli Restore, l'installer ti chiedera il percorso del file ZIP di backup, ripristinera il database, i file e la configurazione.</p>
</div>
<h6>Passo 3: Permessi cartelle</h6>
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache</code></pre>
<h6>Passo 4: Configurare Apache</h6>
<h6 class="mt-4">Passo 4: Configurare Apache (solo modalita Fresh Install)</h6>
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf &lt;&lt;APACHE
&lt;VirtualHost *:80&gt;
ServerName glastree.esempio.it
@@ -861,58 +879,6 @@ APACHE
sudo a2dissite 000-default.conf
sudo a2ensite glastree.conf
sudo systemctl reload apache2</code></pre>
<h6>Passo 5: Configurare l'ambiente</h6>
<pre><code># Copia il file di configurazione di esempio
cp .env.example .env
# Genera la chiave di crittografia (obbligatorio)
php artisan key:generate
# Modifica .env con i parametri del tuo database:
# nano .env
# DB_DATABASE=glastree
# DB_USERNAME=glastree
# DB_PASSWORD=la_tua_password</code></pre>
<h6>Passo 6: Eseguire l'installazione</h6>
<p>Scegli la modalita in base alle tue esigenze:</p>
<h6 class="mt-3"><i class="fas fa-rocket"></i> Fresh Install</h6>
<pre><code># Esegui migration e seed dei dati di base
php artisan migrate --seed
# Crea il collegamento storage
php artisan storage:link
# Crea l'utente amministratore
php artisan tinker --execute="
\$u = new \App\Models\User;
\$u->name = 'Admin';
\$u->email = 'admin@esempio.it';
\$u->password = bcrypt('password_sicura');
\$u->save();
"</code></pre>
<div class="callout callout-info">
<h6><i class="fas fa-info-circle"></i> Dopo l'installazione</h6>
<p>Accedi con le credenziali create, vai in <strong>Impostazioni → Generali</strong> per configurare nome app, logo e parametri email.</p>
</div>
<h6 class="mt-3"><i class="fas fa-upload"></i> Restore da Backup</h6>
<pre><code># Crea il database e importa il backup
# mysql -u root -p glastree &lt; database.sql
# Ripristina i file dei documenti
# tar -xzf storage.tar.gz -C storage/
# Rigenera APP_KEY (invalida dati criptati esistenti!)
php artisan key:generate
# Crea il collegamento storage
php artisan storage:link</code></pre>
<div class="callout alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Importante:</strong> Il restore da backup richiede che tu abbia un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale. Estrai manualmente il contenuto seguendo la struttura del progetto.
</div>
+9 -57
View File
@@ -268,7 +268,7 @@
<h2 id="installazione">5. Guida all'Installazione</h2>
<h3>Panoramica</h3>
<p>L'installazione avviene tramite comandi <strong>Artisan</strong> da terminale. Non esiste un wizard web interattivo. Supporta due modalità:</p>
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalità:</p>
<table>
<thead><tr><th>Modalità</th><th>Descrizione</th></tr></thead>
@@ -313,16 +313,15 @@ git clone &lt;URL_REPOSITORY&gt; glastree
# Opzione B — ZIP (se non hai git)
# unzip /percorso/del/glastree.zip -d glastree
cd glastree
composer install --no-dev --optimize-autoloader
npm ci && npm run build</pre>
<p>Se non hai accesso a npm, puoi saltare il build. L'interfaccia userà comunque AdminLTE via CDN.</p>
cd glastree</pre>
<h4>Passo 3: Permessi cartelle</h4>
<pre>sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache</pre>
<h4>Passo 3: Eseguire l'installer</h4>
<p>Lancia l'installer interattivo che guida attraverso tutte le fasi:</p>
<pre>php install.php</pre>
<p>L'installer chiede: modalità (Fresh Install Apache/Docker o Restore), nome sito, dati amministratore, URL pubblico, database (MySQL o SQLite).<br>
Esegue automaticamente: <code>.env</code> + <code>APP_KEY</code>, composer, migration/seed, admin user, asset (se npm presente), permessi.</p>
<h4>Passo 4: Configurare Apache</h4>
<h4>Passo 4: Configurare Apache (solo Fresh Install)</h4>
<pre>sudo tee /etc/apache2/sites-available/glastree.conf &lt;&lt;APACHE
&lt;VirtualHost *:80&gt;
ServerName glastree.esempio.it
@@ -343,28 +342,7 @@ sudo a2dissite 000-default.conf
sudo a2ensite glastree.conf
sudo systemctl reload apache2</pre>
<h4>Passo 5: Configurare l'ambiente</h4>
<pre># Copia il file di configurazione di esempio
cp .env.example .env
# Genera la chiave di crittografia (obbligatorio)
php artisan key:generate
# Modifica .env con i parametri del tuo database:
# nano .env
# DB_DATABASE=glastree
# DB_USERNAME=glastree
# DB_PASSWORD=la_tua_password</pre>
<h4>Passo 6: Eseguire l'installazione</h4>
<p><strong>Fresh Install:</strong></p>
<pre># Esegui migration e seed dei dati di base
php artisan migrate --seed
# Crea il collegamento storage
php artisan storage:link
# Crea l'utente amministratore
<p>Configura SSL con Let's Encrypt dopo l'installazione.</p>
php artisan tinker --execute="
\$u = new \App\Models\User;
\$u->name = 'Admin';
@@ -386,32 +364,6 @@ php artisan key:generate
# Crea il collegamento storage
php artisan storage:link</pre>
<h3>Modalità Fresh Install</h3>
<p>Seleziona questa modalità quando installi l'applicazione per la prima volta su un server.</p>
<p>Cosa succede durante l'installazione:</p>
<ol>
<li>Genera <code>APP_KEY</code> per la crittografia</li>
<li>Esegue tutte le migration per creare le tabelle del database</li>
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
<li>Crea il collegamento <code>storage public/storage</code></li>
<li>Crea l'utente amministratore con i permessi completi</li>
</ol>
<h3>Modalità Restore da Backup</h3>
<p>Seleziona questa modalità quando vuoi migrare un'installazione esistente su un nuovo server.</p>
<p>Cosa serve:</p>
<ul>
<li>Un file ZIP generato dalla pagina <strong>Admin Backup</strong> del server originale</li>
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
</ul>
<p>Cosa succede durante il restore:</p>
<ol>
<li>Estrae il file ZIP in una cartella temporanea</li>
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
</ol>
<h3>Dopo l'Installazione</h3>
<ol>
<li><strong>Configura SSL</strong> con Let's Encrypt: