Compare commits

..

2 Commits

10 changed files with 214 additions and 15 deletions

View File

@@ -9,9 +9,9 @@ APP_PORT=8080
SEED_DEV_DATA=false SEED_DEV_DATA=false
RUN_DB_SEED_ON_FIRST_START=true RUN_DB_SEED_ON_FIRST_START=true
ENSURE_INITIAL_ADMIN_ON_EMPTY_DB=true ENSURE_INITIAL_ADMIN_ON_EMPTY_DB=true
INITIAL_ADMIN_NAME=Administrator INITIAL_ADMIN_NAME=
INITIAL_ADMIN_EMAIL=info@termanager.it INITIAL_ADMIN_EMAIL=
INITIAL_ADMIN_PASSWORD=Password123! INITIAL_ADMIN_PASSWORD=
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=mariadb DB_HOST=mariadb

3
.gitignore vendored
View File

@@ -2,6 +2,8 @@
/node_modules/ /node_modules/
/.env /.env
/storage/*.key /storage/*.key
/storage/app/.app_key
/storage/app/.db_seeded
/storage/logs/ /storage/logs/
/storage/framework/ /storage/framework/
/bootstrap/cache/ /bootstrap/cache/
@@ -13,6 +15,7 @@
/.vscode/ /.vscode/
*.swp *.swp
*.swo *.swo
*.sql
docker-compose.override.yml docker-compose.override.yml
db_data/ db_data/
redis_data/ redis_data/

View File

@@ -2,8 +2,11 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Database\Seeders\RolesAndPermissionsSeeder;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@@ -19,10 +22,8 @@ class CreateInitialAdmin extends Command
public function handle(): int public function handle(): int
{ {
if (User::count() > 0) { // Always ensure roles/permissions are present before assigning roles.
$this->info('Users already exist. Skipping initial admin creation.'); Artisan::call('db:seed', ['--class' => RolesAndPermissionsSeeder::class, '--force' => true]);
return self::SUCCESS;
}
$name = (string) ($this->option('name') ?? ''); $name = (string) ($this->option('name') ?? '');
$email = (string) ($this->option('email') ?? ''); $email = (string) ($this->option('email') ?? '');
@@ -45,6 +46,30 @@ class CreateInitialAdmin extends Command
$password = $password !== '' ? $password : (string) $this->secret('Password amministratore (min 8 caratteri)'); $password = $password !== '' ? $password : (string) $this->secret('Password amministratore (min 8 caratteri)');
} }
if (User::count() > 0) {
$existingAdmin = User::role('amministratore')->first();
if ($existingAdmin) {
$this->info('An administrator already exists. Skipping initial admin creation.');
return self::SUCCESS;
}
if ($email !== '') {
$existingUser = User::where('email', $email)->first();
if ($existingUser) {
$existingUser->assignRole('amministratore');
$this->info("Granted admin role to existing user: {$existingUser->email}");
return self::SUCCESS;
}
}
$firstUser = User::query()->oldest('id')->first();
if ($firstUser) {
$firstUser->assignRole('amministratore');
$this->warn("No admin role found. Granted admin role to first existing user: {$firstUser->email}");
return self::SUCCESS;
}
}
$validator = Validator::make([ $validator = Validator::make([
'name' => $name, 'name' => $name,
'email' => $email, 'email' => $email,
@@ -63,13 +88,17 @@ class CreateInitialAdmin extends Command
return self::FAILURE; return self::FAILURE;
} }
$admin = User::create([ $admin = DB::transaction(function () use ($name, $email, $password) {
$user = User::create([
'name' => $name, 'name' => $name,
'email' => $email, 'email' => $email,
'password' => Hash::make($password), 'password' => Hash::make($password),
]); ]);
$admin->assignRole('amministratore'); $user->assignRole('amministratore');
return $user;
});
$this->info("Initial admin created: {$admin->email}"); $this->info("Initial admin created: {$admin->email}");

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Livewire\Settings;
use App\Models\User;
use Illuminate\Validation\Rule;
use Livewire\Component;
use Spatie\Permission\Models\Permission;
class UsersIndex extends Component
{
public string $name = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public array $selectedPermissions = [];
public array $availablePermissions = [];
public function mount(): void
{
$this->availablePermissions = Permission::query()
->orderBy('name')
->pluck('name')
->all();
}
protected function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'selectedPermissions' => ['array'],
'selectedPermissions.*' => ['string', Rule::in($this->availablePermissions)],
];
}
public function createUser(): void
{
$validated = $this->validate();
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => $validated['password'],
]);
$user->syncPermissions($validated['selectedPermissions'] ?? []);
$this->reset(['name', 'email', 'password', 'password_confirmation', 'selectedPermissions']);
session()->flash('success', 'Utente creato con successo.');
}
public function render()
{
return view('livewire.settings.users-index', [
'users' => User::query()->with('roles', 'permissions')->orderBy('name')->get(),
]);
}
}

View File

@@ -181,7 +181,7 @@ retry 10 3 php artisan migrate --force
# ----------------------------------------------- # -----------------------------------------------
# 7b. Seed database on first container startup only # 7b. Seed database on first container startup only
# ----------------------------------------------- # -----------------------------------------------
SEED_MARKER_FILE="/var/www/html/storage/app/.db_seeded" SEED_MARKER_FILE="/var/www/html/storage/framework/.runtime_db_seeded"
RUN_DB_SEED_ON_FIRST_START="${RUN_DB_SEED_ON_FIRST_START:-true}" RUN_DB_SEED_ON_FIRST_START="${RUN_DB_SEED_ON_FIRST_START:-true}"
if [ "$RUN_DB_SEED_ON_FIRST_START" = "true" ]; then if [ "$RUN_DB_SEED_ON_FIRST_START" = "true" ]; then

View File

@@ -114,6 +114,11 @@
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg> <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Impostazioni Impostazioni
</a> </a>
<a href="{{ route('users.index') }}"
class="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg {{ request()->routeIs('users.*') ? 'bg-indigo-50 text-indigo-700' : 'text-gray-700 hover:bg-gray-100' }}">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Utenti
</a>
<a href="{{ route('zone.index') }}" <a href="{{ route('zone.index') }}"
class="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg {{ request()->routeIs('zone.*') ? 'bg-indigo-50 text-indigo-700' : 'text-gray-700 hover:bg-gray-100' }}"> class="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg {{ request()->routeIs('zone.*') ? 'bg-indigo-50 text-indigo-700' : 'text-gray-700 hover:bg-gray-100' }}">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg> <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>

View File

@@ -0,0 +1,101 @@
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900">Utenti</h1>
<p class="text-sm text-gray-500 mt-1">Crea utenti e assegna i permessi applicativi.</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Nuovo utente</h2>
<form wire:submit="createUser" class="space-y-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Nome *</label>
<input wire:model="name" id="name" type="text" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
@error('name') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email *</label>
<input wire:model="email" id="email" type="email" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
@error('email') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password *</label>
<input wire:model="password" id="password" type="password" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
@error('password') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label for="password_confirmation" class="block text-sm font-medium text-gray-700">Conferma Password *</label>
<input wire:model="password_confirmation" id="password_confirmation" type="password" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
</div>
</div>
<div>
<p class="block text-sm font-medium text-gray-700 mb-2">Permessi utente</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
@foreach($availablePermissions as $permission)
<label class="flex items-center gap-2 rounded border border-gray-200 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50">
<input wire:model="selectedPermissions" type="checkbox" value="{{ $permission }}" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<span>{{ $permission }}</span>
</label>
@endforeach
</div>
@error('selectedPermissions.*') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition">Crea Utente</button>
</div>
</form>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Utenti esistenti</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left font-medium text-gray-600">Nome</th>
<th class="px-4 py-2 text-left font-medium text-gray-600">Email</th>
<th class="px-4 py-2 text-left font-medium text-gray-600">Ruoli</th>
<th class="px-4 py-2 text-left font-medium text-gray-600">Permessi diretti</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($users as $user)
<tr>
<td class="px-4 py-2 text-gray-900">{{ $user->name }}</td>
<td class="px-4 py-2 text-gray-700">{{ $user->email }}</td>
<td class="px-4 py-2">
<div class="flex flex-wrap gap-1">
@forelse($user->roles as $role)
<span class="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-medium text-indigo-700">{{ $role->name }}</span>
@empty
<span class="text-xs text-gray-400">-</span>
@endforelse
</div>
</td>
<td class="px-4 py-2">
<div class="flex flex-wrap gap-1">
@forelse($user->permissions as $permission)
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700">{{ $permission->name }}</span>
@empty
<span class="text-xs text-gray-400">-</span>
@endforelse
</div>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-6 text-center text-gray-400">Nessun utente trovato</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -24,6 +24,7 @@ use App\Livewire\AuditLog;
use App\Livewire\Settings\SettingsEdit; use App\Livewire\Settings\SettingsEdit;
use App\Livewire\Settings\ZoneIndex; use App\Livewire\Settings\ZoneIndex;
use App\Livewire\Settings\TipologieIndex; use App\Livewire\Settings\TipologieIndex;
use App\Livewire\Settings\UsersIndex;
use App\Livewire\Privacy; use App\Livewire\Privacy;
/* /*
@@ -101,6 +102,7 @@ Route::middleware('auth')->group(function () {
// Settings (admin) // Settings (admin)
Route::middleware('permission:settings.manage')->group(function () { Route::middleware('permission:settings.manage')->group(function () {
Route::get('impostazioni', SettingsEdit::class)->name('settings.edit'); Route::get('impostazioni', SettingsEdit::class)->name('settings.edit');
Route::get('utenti', UsersIndex::class)->name('users.index');
Route::get('zone', ZoneIndex::class)->name('zone.index'); Route::get('zone', ZoneIndex::class)->name('zone.index');
Route::get('tipologie', TipologieIndex::class)->name('tipologie.index'); Route::get('tipologie', TipologieIndex::class)->name('tipologie.index');
}); });

View File

@@ -1 +0,0 @@
base64:7ycOQwH6FjKdElpvJW9JU33pxtNAbOHxGhj6s930X+U=

View File