17 KiB
17 KiB
👤 TABELLA USERS
Autenticazione e collegamento agli amministratori
🎯 CARATTERISTICHE TABELLA
Scopo:
Tabella per autenticazione utenti collegati agli amministratori tramite amministratore_id
Principio:
Ogni user appartiene a un amministratore specifico - nessun dato cross-administrator
🗄️ STRUTTURA TABELLA
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
amministratore_id BIGINT UNSIGNED NOT NULL, -- FK verso amministratori
-- Dati autenticazione
username VARCHAR(100) NOT NULL, -- Username univoco per amministratore
email VARCHAR(255) NOT NULL,
email_verified_at TIMESTAMP NULL,
password VARCHAR(255) NOT NULL,
remember_token VARCHAR(100) NULL,
-- Dati personali
nome VARCHAR(255) NOT NULL,
cognome VARCHAR(255) NOT NULL,
telefono VARCHAR(20) NULL,
-- Ruoli e permessi (semplificato con enum)
ruolo ENUM('admin', 'collaboratore', 'visualizzatore') DEFAULT 'collaboratore',
-- Configurazioni utente
is_active BOOLEAN DEFAULT TRUE,
ultimo_accesso TIMESTAMP NULL,
preferenze JSON NULL, -- Preferenze UI/UX
-- Reset password
password_reset_token VARCHAR(100) NULL,
password_reset_expires_at TIMESTAMP NULL,
-- Audit
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Foreign Key
FOREIGN KEY (amministratore_id) REFERENCES amministratori(id) ON DELETE CASCADE,
-- Indici
UNIQUE KEY unique_username_per_admin (amministratore_id, username),
UNIQUE KEY unique_email_per_admin (amministratore_id, email),
INDEX idx_amministratore_id (amministratore_id),
INDEX idx_ruolo (ruolo),
INDEX idx_active (is_active),
INDEX idx_ultimo_accesso (ultimo_accesso)
);
🔐 AUTHENTICATION PATTERNS
Login Multi-Tenant:
L'accesso richiede:
- Username/Email
- Password
- Codice Amministratore (8 cifre)
// Login form deve avere:
// - username/email
// - password
// - codice_amministratore (8 cifre)
public function authenticate(LoginRequest $request)
{
$credentials = $request->only(['username', 'password']);
$codiceAmministratore = $request->input('codice_amministratore');
// Trova amministratore dal codice
$amministratore = Amministratore::where('codice_univoco', $codiceAmministratore)
->where('is_active', true)
->first();
if (!$amministratore) {
throw ValidationException::withMessages([
'codice_amministratore' => 'Codice amministratore non valido.'
]);
}
// Trova user nell'ambito dell'amministratore
$user = User::where('amministratore_id', $amministratore->id)
->where(function($query) use ($credentials) {
$query->where('username', $credentials['username'])
->orWhere('email', $credentials['username']);
})
->where('is_active', true)
->first();
if (!$user || !Hash::check($credentials['password'], $user->password)) {
throw ValidationException::withMessages([
'username' => 'Credenziali non valide.'
]);
}
Auth::login($user);
$user->updateUltimoAccesso();
return redirect()->intended('/dashboard');
}
🏗️ MIGRATION LARAVEL
File: database/migrations/2024_08_05_000002_create_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->foreignId('amministratore_id')
->constrained('amministratori')
->onDelete('cascade');
// Dati autenticazione
$table->string('username', 100);
$table->string('email');
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
// Dati personali
$table->string('nome');
$table->string('cognome');
$table->string('telefono', 20)->nullable();
// Ruoli e permessi
$table->enum('ruolo', ['admin', 'collaboratore', 'visualizzatore'])
->default('collaboratore');
// Configurazioni utente
$table->boolean('is_active')->default(true);
$table->timestamp('ultimo_accesso')->nullable();
$table->json('preferenze')->nullable();
// Reset password
$table->string('password_reset_token', 100)->nullable();
$table->timestamp('password_reset_expires_at')->nullable();
$table->timestamps();
// Indici e vincoli
$table->unique(['amministratore_id', 'username'], 'unique_username_per_admin');
$table->unique(['amministratore_id', 'email'], 'unique_email_per_admin');
$table->index(['amministratore_id']);
$table->index(['ruolo']);
$table->index(['is_active']);
$table->index(['ultimo_accesso']);
});
}
public function down(): void
{
Schema::dropIfExists('users');
}
};
📊 MODEL ELOQUENT
File: app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'amministratore_id',
'username',
'email',
'password',
'nome',
'cognome',
'telefono',
'ruolo',
'is_active',
'preferenze',
];
protected $hidden = [
'password',
'remember_token',
'password_reset_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'ultimo_accesso' => 'datetime',
'password_reset_expires_at' => 'datetime',
'is_active' => 'boolean',
'preferenze' => 'array',
];
// Relazioni
public function amministratore(): BelongsTo
{
return $this->belongsTo(Amministratore::class);
}
// Scope
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
public function scopeByRuolo(Builder $query, string $ruolo): Builder
{
return $query->where('ruolo', $ruolo);
}
public function scopeForAmministratore(Builder $query, int $amministratoreId): Builder
{
return $query->where('amministratore_id', $amministratoreId);
}
// Accessors
public function getNomeCompletoAttribute(): string
{
return "{$this->nome} {$this->cognome}";
}
public function getIsAdminAttribute(): bool
{
return $this->ruolo === 'admin';
}
public function getCanEditAttribute(): bool
{
return in_array($this->ruolo, ['admin', 'collaboratore']);
}
public function getCanViewOnlyAttribute(): bool
{
return $this->ruolo === 'visualizzatore';
}
// Helper Methods
public function hasPermission(string $permission): bool
{
$permissions = [
'admin' => ['view', 'create', 'edit', 'delete', 'manage_users'],
'collaboratore' => ['view', 'create', 'edit'],
'visualizzatore' => ['view']
];
return in_array($permission, $permissions[$this->ruolo] ?? []);
}
public function updateUltimoAccesso(): void
{
$this->update(['ultimo_accesso' => now()]);
}
public function generatePasswordResetToken(): string
{
$token = bin2hex(random_bytes(32));
$this->update([
'password_reset_token' => $token,
'password_reset_expires_at' => now()->addHours(24)
]);
return $token;
}
public function isPasswordResetTokenValid(string $token): bool
{
return $this->password_reset_token === $token &&
$this->password_reset_expires_at &&
$this->password_reset_expires_at->isFuture();
}
public function clearPasswordResetToken(): void
{
$this->update([
'password_reset_token' => null,
'password_reset_expires_at' => null
]);
}
}
🌱 SEEDER DATI DEMO
File: database/seeders/UsersSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Amministratore;
use Illuminate\Support\Facades\Hash;
class UsersSeeder extends Seeder
{
public function run(): void
{
$amministratori = Amministratore::all();
foreach ($amministratori as $admin) {
// User admin per ogni amministratore
User::create([
'amministratore_id' => $admin->id,
'username' => 'admin',
'email' => $admin->email,
'password' => Hash::make('password123'),
'nome' => $admin->nome,
'cognome' => $admin->cognome,
'telefono' => $admin->telefono,
'ruolo' => 'admin',
'is_active' => true,
'email_verified_at' => now(),
]);
// User collaboratore
User::create([
'amministratore_id' => $admin->id,
'username' => 'collaboratore1',
'email' => "collaboratore.{$admin->codice_univoco}@netgescon.it",
'password' => Hash::make('password123'),
'nome' => 'Marco',
'cognome' => 'Collaboratore',
'telefono' => '3331112233',
'ruolo' => 'collaboratore',
'is_active' => true,
'email_verified_at' => now(),
]);
// User visualizzatore
User::create([
'amministratore_id' => $admin->id,
'username' => 'viewer1',
'email' => "viewer.{$admin->codice_univoco}@netgescon.it",
'password' => Hash::make('password123'),
'nome' => 'Luca',
'cognome' => 'Viewer',
'ruolo' => 'visualizzatore',
'is_active' => true,
'email_verified_at' => now(),
]);
}
$totalUsers = User::count();
$this->command->info("✅ Creati {$totalUsers} users per gli amministratori");
}
}
🎛️ MIDDLEWARE TENANT
File: app/Http/Middleware/TenantScope.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class TenantScope
{
public function handle(Request $request, Closure $next)
{
if (!Auth::check()) {
return redirect()->route('login');
}
$user = Auth::user();
// Verifica che l'amministratore sia attivo
if (!$user->amministratore || !$user->amministratore->is_active) {
Auth::logout();
return redirect()->route('login')
->withErrors(['account' => 'Account amministratore non attivo.']);
}
// Imposta il tenant globale per le query
app()->singleton('current_amministratore', function () use ($user) {
return $user->amministratore;
});
return $next($request);
}
}
📋 FORM LOGIN
File: resources/views/auth/login.blade.php
@extends('layouts.guest')
@section('content')
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Accesso NetGescon
</h2>
</div>
<form class="mt-8 space-y-6" method="POST" action="{{ route('login') }}">
@csrf
<!-- Codice Amministratore -->
<div>
<label for="codice_amministratore" class="block text-sm font-medium text-gray-700">
Codice Amministratore (8 cifre)
</label>
<input id="codice_amministratore"
name="codice_amministratore"
type="text"
maxlength="8"
required
class="mt-1 appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm @error('codice_amministratore') border-red-500 @enderror"
placeholder="ES: A1B2C3D4"
value="{{ old('codice_amministratore') }}">
@error('codice_amministratore')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Username/Email -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700">
Username o Email
</label>
<input id="username"
name="username"
type="text"
required
class="mt-1 appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm @error('username') border-red-500 @enderror"
placeholder="Username o email"
value="{{ old('username') }}">
@error('username')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Password -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700">
Password
</label>
<input id="password"
name="password"
type="password"
required
class="mt-1 appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm @error('password') border-red-500 @enderror"
placeholder="Password">
@error('password')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Remember me -->
<div class="flex items-center justify-between">
<div class="flex items-center">
<input id="remember" name="remember" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="remember" class="ml-2 block text-sm text-gray-900">
Ricordami
</label>
</div>
</div>
<div>
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Accedi
</button>
</div>
</form>
</div>
</div>
@endsection
🎯 UTILIZZO PATTERN
Accesso ai Dati Tenant:
// Nel controller - sempre partire dall'utente loggato
$amministratore = auth()->user()->amministratore;
// Query scope automatic
$stabili = Stabile::where('amministratore_id', $amministratore->id)->get();
// Helper globale
$amministratore = app('current_amministratore');
Permessi per Ruolo:
// Nel controller
if (!auth()->user()->hasPermission('edit')) {
abort(403, 'Non hai il permesso per modificare questi dati.');
}
// Nel blade
@can('edit')
<a href="{{ route('stabili.edit', $stabile) }}">Modifica</a>
@endcan
🎯 TABELLA USERS DEFINITA
Con autenticazione multi-tenant, ruoli e collegamento amministratori
📖 Prossimo: Tabelle collegate (stabili, gestioni, unità)