562 lines
17 KiB
Markdown
562 lines
17 KiB
Markdown
# 👤 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**
|
|
|
|
```sql
|
|
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:
|
|
1. **Username/Email**
|
|
2. **Password**
|
|
3. **Codice Amministratore** (8 cifre)
|
|
|
|
```php
|
|
// 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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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`
|
|
|
|
```blade
|
|
@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**:
|
|
```php
|
|
// 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**:
|
|
```php
|
|
// 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à)**
|