This commit is contained in:
2026-04-05 20:14:30 +00:00
parent 22ac0aa781
commit 1606778518
12 changed files with 130 additions and 57 deletions

View File

@@ -6,6 +6,7 @@ APP_TIMEZONE=Europe/Rome
APP_URL=http://localhost:8080
APP_PORT=8080
SEED_DEV_DATA=false
DB_CONNECTION=mysql
DB_HOST=mariadb

View File

@@ -139,6 +139,7 @@ docker compose up -d --build
|-----------------------|----------------------|------------------------------------------|
| `APP_KEY` | (generata) | Chiave AES-256 per cifratura. **Mai condividere** |
| `APP_PORT` | `8080` | Porta host per l'applicazione |
| `SEED_DEV_DATA` | `false` | Se `true`, `php artisan db:seed` include anche i dati demo |
| `DB_DATABASE` | `termanager2` | Nome database MariaDB |
| `DB_USERNAME` | `termanager2` | Utente database |
| `DB_PASSWORD` | `secret` | Password database |

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
class LoginController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
if (! Setting::isSetupComplete() || User::count() === 0) {
return redirect()->route('setup.index');
}
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string', 'min:6'],
'remember' => ['nullable', 'boolean'],
]);
$throttleKey = Str::transliterate(Str::lower($credentials['email']) . '|' . $request->ip());
if (RateLimiter::tooManyAttempts($throttleKey, 5)) {
$seconds = RateLimiter::availableIn($throttleKey);
return back()
->withErrors(['email' => "Troppi tentativi. Riprova tra {$seconds} secondi."])
->withInput($request->only('email', 'remember'));
}
if (! Auth::attempt([
'email' => $credentials['email'],
'password' => $credentials['password'],
], $request->boolean('remember'))) {
RateLimiter::hit($throttleKey);
return back()
->withErrors(['email' => 'Credenziali non valide.'])
->withInput($request->only('email', 'remember'));
}
RateLimiter::clear($throttleKey);
$request->session()->regenerate();
activity()->causedBy(auth()->user())->log('login');
return redirect()->intended(route('dashboard'));
}
}

View File

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

View File

@@ -3,47 +3,16 @@
namespace App\Livewire\Auth;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use App\Models\Setting;
use App\Models\User;
class Login extends Component
{
public string $email = '';
public string $password = '';
public bool $remember = false;
protected function rules(): array
public function mount()
{
return [
'email' => 'required|email',
'password' => 'required|min:6',
];
if (! Setting::isSetupComplete() || User::count() === 0) {
return redirect()->route('setup.index');
}
public function login()
{
$this->validate();
$throttleKey = Str::transliterate(Str::lower($this->email) . '|' . request()->ip());
if (RateLimiter::tooManyAttempts($throttleKey, 5)) {
$seconds = RateLimiter::availableIn($throttleKey);
$this->addError('email', "Troppi tentativi. Riprova tra {$seconds} secondi.");
return;
}
if (!Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
RateLimiter::hit($throttleKey);
$this->addError('email', 'Credenziali non valide.');
return;
}
RateLimiter::clear($throttleKey);
session()->regenerate();
activity()->causedBy(auth()->user())->log('login');
return redirect()->intended(route('dashboard'));
}
public function render()

View File

@@ -6,6 +6,7 @@ use Livewire\Component;
use Livewire\WithFileUploads;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class Wizard extends Component
@@ -33,11 +34,11 @@ class Wizard extends Component
public function mount()
{
if (Setting::isSetupComplete()) {
if (Setting::isSetupComplete() && User::count() > 0) {
return redirect()->route('dashboard');
}
$this->needsAdmin = User::count() <= 1;
$this->needsAdmin = User::count() === 0;
$setting = Setting::first();
if ($setting) {
@@ -107,12 +108,19 @@ class Wizard extends Component
'password' => Hash::make($this->admin_password),
]);
$admin->assignRole('amministratore');
Auth::login($admin);
request()->session()->regenerate();
}
session()->flash('success', 'Setup completato con successo!');
if (auth()->check()) {
return redirect()->route('dashboard');
}
return redirect()->route('login');
}
public function render()
{
return view('livewire.setup.wizard')

View File

@@ -3,14 +3,16 @@
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\App;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
RolesAndPermissionsSeeder::class,
DevSeeder::class,
]);
$this->call([RolesAndPermissionsSeeder::class]);
if (App::environment('local') && env('SEED_DEV_DATA', false)) {
$this->call([DevSeeder::class]);
}
}
}

View File

@@ -16,6 +16,12 @@ services:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "nc -z 127.0.0.1 9000"]
interval: 5s
timeout: 3s
retries: 20
start_period: 45s
environment:
- PHP_OPCACHE_VALIDATE_TIMESTAMPS=1
@@ -32,7 +38,8 @@ services:
networks:
- termanager2
depends_on:
- app
app:
condition: service_healthy
mariadb:
image: mariadb:11

View File

@@ -4,6 +4,7 @@ FROM php:8.3-fpm
RUN apt-get update && apt-get install -y \
git \
curl \
netcat-openbsd \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
@@ -58,6 +59,7 @@ RUN if [ -f package-lock.json ]; then \
COPY . .
RUN mkdir -p bootstrap/cache storage/framework/cache storage/framework/sessions storage/framework/views storage/logs storage/app
RUN if [ ! -f .env ] && [ -f .env.example ]; then cp .env.example .env; fi
RUN date -u +%Y%m%d%H%M%S > .image-build-id
RUN composer dump-autoload --optimize --no-interaction
RUN npm run build

View File

@@ -37,12 +37,34 @@ upsert_env() {
fi
}
sync_app_code() {
local env_backup="/tmp/termanager2.env.backup"
rm -f "$env_backup"
if [ -f /var/www/html/.env ]; then
cp /var/www/html/.env "$env_backup"
fi
cp -a /app-src/. /var/www/html/
if [ -f "$env_backup" ]; then
mv "$env_backup" /var/www/html/.env
fi
}
# -----------------------------------------------
# 0. Sync application code from image to volume
# -----------------------------------------------
IMAGE_BUILD_FILE="/app-src/.image-build-id"
VOLUME_BUILD_FILE="/var/www/html/.image-build-id"
if [ ! -f /var/www/html/artisan ]; then
echo "[*] Syncing application code to volume..."
cp -a /app-src/. /var/www/html/
sync_app_code
elif [ -f "$IMAGE_BUILD_FILE" ] && { [ ! -f "$VOLUME_BUILD_FILE" ] || ! cmp -s "$IMAGE_BUILD_FILE" "$VOLUME_BUILD_FILE"; }; then
echo "[*] New image detected. Syncing updated application code to volume..."
sync_app_code
else
echo "[✓] Application code already in volume."
fi

View File

@@ -5,34 +5,31 @@
<p class="text-gray-500 text-sm mt-1">Accedi per continuare</p>
</div>
<form wire:submit="login" class="space-y-5">
<form method="POST" action="{{ route('login.store') }}" class="space-y-5">
@csrf
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input wire:model="email" type="email" id="email" autocomplete="email"
<input name="email" value="{{ old('email') }}" type="email" id="email" autocomplete="email"
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm px-4 py-2.5">
@error('email') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input wire:model="password" type="password" id="password" autocomplete="current-password"
<input name="password" type="password" id="password" autocomplete="current-password"
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm px-4 py-2.5">
@error('password') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div class="flex items-center">
<input wire:model="remember" type="checkbox" id="remember"
<input name="remember" value="1" {{ old('remember') ? 'checked' : '' }} type="checkbox" id="remember"
class="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
<label for="remember" class="ml-2 text-sm text-gray-600">Ricordami</label>
</div>
<button type="submit"
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition">
<span wire:loading.remove>Accedi</span>
<span wire:loading class="flex items-center gap-2">
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
Accesso in corso...
</span>
<span>Accedi</span>
</button>
</form>
</div>

View File

@@ -1,6 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\LoginController;
use App\Livewire\Home;
use App\Livewire\Setup\Wizard;
use App\Livewire\Territori\TerritorioIndex;
@@ -33,6 +34,7 @@ use App\Livewire\Privacy;
*/
Route::middleware('guest')->group(function () {
Route::get('login', App\Livewire\Auth\Login::class)->name('login');
Route::post('login', LoginController::class)->name('login.store');
});
Route::post('logout', function () {
@@ -48,9 +50,9 @@ Route::post('logout', function () {
| Setup Wizard (bypasses setup.required middleware via exclusion)
|--------------------------------------------------------------------------
*/
Route::middleware('auth')->group(function () {
Route::get('setup', Wizard::class)->name('setup.index')->withoutMiddleware(\App\Http\Middleware\SetupRequired::class);
});
Route::get('setup', Wizard::class)
->name('setup.index')
->withoutMiddleware(\App\Http\Middleware\SetupRequired::class);
/*
|--------------------------------------------------------------------------