feat: Implement client authentication and pre-fill functionality
- Added AuthController for client login and token management. - Introduced AuthGate for handling authentication state and login UI. - Updated API to support fetching client data for pre-filling forms. - Modified App.vue to pre-fill client information if logged in. - Enhanced client model to include username and password fields. - Created migration for adding username and password to clients table. - Added utility for generating secure usernames and passwords. - Updated frontend to handle client tokens for pre-filling checkout forms. - Removed old CSS file and updated HTML references to new assets. - Added StatsOverview widget for displaying client and quote statistics. - Improved error handling and user feedback in the login process.
This commit is contained in:
@@ -4,8 +4,11 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ClienteResource\Pages;
|
||||
use App\Models\Cliente;
|
||||
use App\Support\ClienteCredenziali;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Infolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
@@ -27,16 +30,54 @@ class ClienteResource extends Resource
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
Forms\Components\TextInput::make('nome')->label('Nome')->required(),
|
||||
Forms\Components\TextInput::make('cognome')->label('Cognome'),
|
||||
Forms\Components\TextInput::make('ragione_sociale')->label('Ragione sociale'),
|
||||
Forms\Components\TextInput::make('email')->label('Email')->email()->required(),
|
||||
Forms\Components\TextInput::make('telefono')->label('Telefono')->tel(),
|
||||
Forms\Components\TextInput::make('indirizzo')->label('Indirizzo'),
|
||||
Forms\Components\TextInput::make('cap')->label('CAP')->maxLength(10),
|
||||
Forms\Components\TextInput::make('citta')->label('Città'),
|
||||
Forms\Components\TextInput::make('provincia')->label('Provincia')->maxLength(5),
|
||||
Forms\Components\Textarea::make('note')->label('Note')->columnSpanFull(),
|
||||
Forms\Components\Section::make('Accesso')
|
||||
->description('Credenziali per l\'accesso del cliente. Generate automaticamente, ma modificabili.')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('username')
|
||||
->label('Nome utente')
|
||||
->required()
|
||||
->maxLength(50)
|
||||
->unique(ignoreRecord: true)
|
||||
->default(fn (string $operation) => $operation === 'create' ? ClienteCredenziali::generaUsername() : null)
|
||||
->suffixAction(
|
||||
Forms\Components\Actions\Action::make('rigeneraUsername')
|
||||
->icon('heroicon-m-arrow-path')
|
||||
->tooltip('Genera un nuovo nome utente')
|
||||
->action(fn (Forms\Set $set) => $set('username', ClienteCredenziali::generaUsername()))
|
||||
),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->label('Password')
|
||||
->password()
|
||||
->revealable()
|
||||
->required(fn (string $operation) => $operation === 'create')
|
||||
->dehydrated(fn (?string $state) => filled($state))
|
||||
->default(fn (string $operation) => $operation === 'create' ? ClienteCredenziali::generaPassword() : null)
|
||||
->suffixAction(
|
||||
Forms\Components\Actions\Action::make('rigeneraPassword')
|
||||
->icon('heroicon-m-arrow-path')
|
||||
->tooltip('Genera una nuova password')
|
||||
->action(fn (Forms\Set $set) => $set('password', ClienteCredenziali::generaPassword()))
|
||||
)
|
||||
->helperText(fn (string $operation) => $operation === 'create'
|
||||
? 'Generata automaticamente. Ricordati di copiarla dopo il salvataggio: non sarà più leggibile.'
|
||||
: 'Lascia vuoto per mantenere la password attuale, oppure genera/inserisci una nuova password.'),
|
||||
]),
|
||||
Forms\Components\Section::make('Anagrafica')
|
||||
->description('Campi facoltativi.')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('nome')->label('Nome'),
|
||||
Forms\Components\TextInput::make('cognome')->label('Cognome'),
|
||||
Forms\Components\TextInput::make('ragione_sociale')->label('Ragione sociale'),
|
||||
Forms\Components\TextInput::make('email')->label('Email')->email(),
|
||||
Forms\Components\TextInput::make('telefono')->label('Telefono')->tel(),
|
||||
Forms\Components\TextInput::make('indirizzo')->label('Indirizzo'),
|
||||
Forms\Components\TextInput::make('cap')->label('CAP')->maxLength(10),
|
||||
Forms\Components\TextInput::make('citta')->label('Città'),
|
||||
Forms\Components\TextInput::make('provincia')->label('Provincia')->maxLength(5),
|
||||
Forms\Components\Textarea::make('note')->label('Note')->columnSpanFull(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -46,13 +87,15 @@ class ClienteResource extends Resource
|
||||
->defaultSort('created_at', 'desc')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('nominativo')->label('Nominativo')->searchable(['nome', 'cognome', 'ragione_sociale']),
|
||||
Tables\Columns\TextColumn::make('email')->label('Email')->searchable()->copyable(),
|
||||
Tables\Columns\TextColumn::make('username')->label('Nome utente')->searchable()->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('email')->label('Email')->searchable()->copyable()->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('telefono')->label('Telefono')->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('citta')->label('Città')->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('preventivi_count')->label('Preventivi')->counts('preventivi'),
|
||||
Tables\Columns\TextColumn::make('created_at')->label('Registrato')->dateTime('d/m/Y')->sortable(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
@@ -62,10 +105,42 @@ class ClienteResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
public static function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist->schema([
|
||||
Infolists\Components\View::make('filament.infolists.cliente-password-reveal')
|
||||
->visible(fn () => filled(session('cliente_password_plain')))
|
||||
->viewData(fn () => [
|
||||
'username' => session('cliente_username_plain'),
|
||||
'password' => session('cliente_password_plain'),
|
||||
]),
|
||||
Infolists\Components\Section::make('Accesso')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('username')->label('Nome utente')->copyable()->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('created_at')->label('Cliente dal')->dateTime('d/m/Y H:i'),
|
||||
]),
|
||||
Infolists\Components\Section::make('Anagrafica')
|
||||
->columns(3)
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('nominativo')->label('Nominativo')->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('email')->label('Email')->copyable()->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('telefono')->label('Telefono')->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('indirizzo')->label('Indirizzo')->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('cap')->label('CAP')->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('citta')->label('Città')->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('provincia')->label('Provincia')->placeholder('—'),
|
||||
Infolists\Components\TextEntry::make('note')->label('Note')->columnSpanFull()->placeholder('—'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListClientes::route('/'),
|
||||
'create' => Pages\CreateCliente::route('/create'),
|
||||
'view' => Pages\ViewCliente::route('/{record}'),
|
||||
'edit' => Pages\EditCliente::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,10 +3,31 @@
|
||||
namespace App\Filament\Resources\ClienteResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ClienteResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class CreateCliente extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ClienteResource::class;
|
||||
|
||||
protected ?string $plainPassword = null;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->plainPassword = $data['password'];
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
session()->flash('cliente_username_plain', $this->record->username);
|
||||
session()->flash('cliente_password_plain', $this->plainPassword);
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('view', ['record' => $this->getRecord()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ namespace App\Filament\Resources\ClienteResource\Pages;
|
||||
use App\Filament\Resources\ClienteResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class EditCliente extends EditRecord
|
||||
{
|
||||
protected static string $resource = ClienteResource::class;
|
||||
|
||||
protected ?string $plainPassword = null;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -17,4 +20,29 @@ class EditCliente extends EditRecord
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (filled($data['password'] ?? null)) {
|
||||
$this->plainPassword = $data['password'];
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
} else {
|
||||
unset($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
if ($this->plainPassword !== null) {
|
||||
session()->flash('cliente_username_plain', $this->record->username);
|
||||
session()->flash('cliente_password_plain', $this->plainPassword);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('view', ['record' => $this->getRecord()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Filament\Resources\ClienteResource;
|
||||
use App\Filament\Resources\PreventivoResource;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\Preventivo;
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
|
||||
class StatsOverview extends BaseWidget
|
||||
{
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [
|
||||
Stat::make('Preventivi', Preventivo::count())
|
||||
->icon('heroicon-o-document-text')
|
||||
->color('warning')
|
||||
->url(PreventivoResource::getUrl('index')),
|
||||
Stat::make('Clienti', Cliente::count())
|
||||
->icon('heroicon-o-users')
|
||||
->color('success')
|
||||
->url(ClienteResource::getUrl('index')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Cliente;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
* Login del cliente per l'accesso al configuratore (username + password).
|
||||
*/
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'username' => ['required', 'string'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$cliente = Cliente::where('username', $data['username'])->first();
|
||||
|
||||
if (! $cliente || ! $cliente->password || ! password_verify($data['password'], $cliente->password)) {
|
||||
return response()->json([
|
||||
'message' => 'Nome utente o password non validi.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$giorni = (int) config('preventivi.configurator_session_days', 30);
|
||||
$token = $cliente->createToken('configuratore', ['*'], now()->addDays($giorni));
|
||||
|
||||
return response()->json([
|
||||
'token' => $token->plainTextToken,
|
||||
'expires_at' => $token->accessToken->expires_at,
|
||||
'cliente' => [
|
||||
'id' => $cliente->id,
|
||||
'username' => $cliente->username,
|
||||
'nominativo' => $cliente->nominativo,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
* Verifica che il token del configuratore sia ancora valido e restituisce i dati
|
||||
* anagrafici del cliente, usati per precompilare il form di richiesta preventivo.
|
||||
*/
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
$cliente = $request->user();
|
||||
|
||||
return response()->json([
|
||||
'cliente' => [
|
||||
'id' => $cliente->id,
|
||||
'username' => $cliente->username,
|
||||
'nominativo' => $cliente->nominativo,
|
||||
'nome' => $cliente->nome,
|
||||
'cognome' => $cliente->cognome,
|
||||
'ragione_sociale' => $cliente->ragione_sociale,
|
||||
'email' => $cliente->email,
|
||||
'telefono' => $cliente->telefono,
|
||||
'indirizzo' => $cliente->indirizzo,
|
||||
'cap' => $cliente->cap,
|
||||
'citta' => $cliente->citta,
|
||||
'provincia' => $cliente->provincia,
|
||||
'note' => $cliente->note,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,28 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class Cliente extends Model
|
||||
{
|
||||
use HasApiTokens;
|
||||
|
||||
protected $table = 'clienti';
|
||||
|
||||
protected $fillable = [
|
||||
'username', 'password',
|
||||
'nome', 'cognome', 'ragione_sociale', 'email', 'telefono',
|
||||
'indirizzo', 'cap', 'citta', 'provincia', 'note',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'password' => 'hashed',
|
||||
];
|
||||
|
||||
public function preventivi(): HasMany
|
||||
{
|
||||
return $this->hasMany(Preventivo::class);
|
||||
@@ -25,6 +37,8 @@ class Cliente extends Model
|
||||
return $this->ragione_sociale;
|
||||
}
|
||||
|
||||
return trim("{$this->nome} {$this->cognome}");
|
||||
$nome = trim("{$this->nome} {$this->cognome}");
|
||||
|
||||
return $nome !== '' ? $nome : $this->username;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Filament\Pages;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Widgets;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
@@ -41,10 +40,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
Pages\Dashboard::class,
|
||||
])
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||
->widgets([
|
||||
Widgets\AccountWidget::class,
|
||||
Widgets\FilamentInfoWidget::class,
|
||||
])
|
||||
->widgets([])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Cliente;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ClienteCredenziali
|
||||
{
|
||||
/** Genera un nome utente tipo "user2130" (numero casuale, minimo 4 cifre) non ancora in uso. */
|
||||
public static function generaUsername(): string
|
||||
{
|
||||
do {
|
||||
$username = 'user'.random_int(1000, 999999);
|
||||
} while (Cliente::where('username', $username)->exists());
|
||||
|
||||
return $username;
|
||||
}
|
||||
|
||||
/** Genera una password casuale sicura (lettere maiuscole/minuscole, numeri, simboli). */
|
||||
public static function generaPassword(): string
|
||||
{
|
||||
return Str::password(14);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user