netgescon-day0/docs/91-database/01-struttura-principale/users.md

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:

  1. Username/Email
  2. Password
  3. 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à)