feat(auth): Implementazione completa sistema autenticazione

BREAKING CHANGE: Tutte le pagine ora richiedono autenticazione

Nuove funzionalità:
- Sistema di login con password hardcoded (admin123)
- Form di login full-screen con gradiente viola
- Protezione automatica di tutte le route
- Pulsante logout visibile in tutte le pagine
- Gestione thread-safe eventi autenticazione con InvokeAsync()

Componenti:
- AuthenticationService: servizio Singleton per gestione stato
- Login.razor: pagina login con validazione e messaggi errore
- App.razor: routing condizionale basato su autenticazione
- MainLayout.razor: pulsante logout integrato

Fix tecnici:
- Risolto errore "Dispatcher not associated" usando InvokeAsync()
- Implementato pattern corretto per eventi cross-thread in Blazor Server
- Aggiunto Dispose per prevenire memory leak
This commit is contained in:
Alessio Dal Santo
2025-10-08 17:58:46 +02:00
parent 960166be9f
commit 22c0a15b8e
10 changed files with 1043 additions and 14 deletions
+56 -12
View File
@@ -1,12 +1,56 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
@using Data_Coupler.Services
@using Data_Coupler.Pages
@inject IAuthenticationService AuthService
@implements IDisposable
@if (!AuthService.IsAuthenticated)
{
<Login />
}
else
{
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
@{
// Se l'utente prova ad accedere alla pagina di login mentre è autenticato, reindirizza alla home
if (routeData.PageType == typeof(Data_Coupler.Pages.Login))
{
<RouteView RouteData="@CreateHomeRouteData()" DefaultLayout="@typeof(MainLayout)" />
}
else
{
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
}
}
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
}
@code {
protected override void OnInitialized()
{
AuthService.OnAuthenticationStateChanged += OnAuthenticationStateChanged;
}
public void Dispose()
{
AuthService.OnAuthenticationStateChanged -= OnAuthenticationStateChanged;
}
private void OnAuthenticationStateChanged()
{
InvokeAsync(StateHasChanged);
}
private RouteData CreateHomeRouteData()
{
return new RouteData(typeof(Data_Coupler.Pages.DataCoupler), new Dictionary<string, object?>());
}
}
+181
View File
@@ -0,0 +1,181 @@
@page "/login"
@using Data_Coupler.Services
@using Microsoft.AspNetCore.Components.Forms
@inject IAuthenticationService AuthService
@inject NavigationManager NavigationManager
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h2>Data Coupler</h2>
<p>Accedi per continuare</p>
</div>
<div class="login-body">
<EditForm Model="@loginModel" OnValidSubmit="@HandleLogin">
<DataAnnotationsValidator />
<div class="form-group">
<label for="password">Password</label>
<InputText type="password"
class="form-control"
id="password"
@bind-Value="loginModel.Password"
placeholder="Inserisci la password"
autocomplete="current-password" />
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger" role="alert">
@errorMessage
</div>
}
<button type="submit" class="btn btn-primary btn-block">
<i class="oi oi-account-login"></i> Accedi
</button>
</EditForm>
</div>
</div>
</div>
<style>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
overflow: hidden;
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.login-header h2 {
margin: 0;
font-size: 28px;
font-weight: 600;
}
.login-header p {
margin: 10px 0 0 0;
opacity: 0.9;
}
.login-body {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn-block {
width: 100%;
padding: 12px;
font-size: 16px;
font-weight: 500;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.alert {
padding: 12px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert-danger {
background-color: #fee;
border: 1px solid #fcc;
color: #c33;
}
</style>
@code {
private LoginModel loginModel = new LoginModel();
private string errorMessage = string.Empty;
protected override void OnInitialized()
{
// Se l'utente è già autenticato, reindirizza alla home
if (AuthService.IsAuthenticated)
{
NavigationManager.NavigateTo("/");
}
}
private void HandleLogin()
{
errorMessage = string.Empty;
if (string.IsNullOrWhiteSpace(loginModel.Password))
{
errorMessage = "Inserisci la password";
return;
}
if (AuthService.Login(loginModel.Password))
{
NavigationManager.NavigateTo("/");
}
else
{
errorMessage = "Password non corretta";
loginModel.Password = string.Empty;
}
}
private class LoginModel
{
public string Password { get; set; } = string.Empty;
}
}
+3
View File
@@ -29,6 +29,9 @@ builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddWindowsService();
// Register Authentication Service
builder.Services.AddSingleton<Data_Coupler.Services.IAuthenticationService, Data_Coupler.Services.AuthenticationService>();
// Configurazione logging per Windows Service
if (OperatingSystem.IsWindows())
{
@@ -0,0 +1,41 @@
using System;
namespace Data_Coupler.Services
{
public interface IAuthenticationService
{
bool IsAuthenticated { get; }
event Action? OnAuthenticationStateChanged;
bool Login(string password);
void Logout();
}
public class AuthenticationService : IAuthenticationService
{
// Password hardcoded - CAMBIARE IN PRODUZIONE
private const string HARDCODED_PASSWORD = "admin123";
private bool _isAuthenticated = false;
public bool IsAuthenticated => _isAuthenticated;
public event Action? OnAuthenticationStateChanged;
public bool Login(string password)
{
if (password == HARDCODED_PASSWORD)
{
_isAuthenticated = true;
OnAuthenticationStateChanged?.Invoke();
return true;
}
return false;
}
public void Logout()
{
_isAuthenticated = false;
OnAuthenticationStateChanged?.Invoke();
}
}
}
+19 -2
View File
@@ -1,4 +1,6 @@
@inherits LayoutComponentBase
@using Data_Coupler.Services
@inject IAuthenticationService AuthService
@inherits LayoutComponentBase
<PageTitle>Data_Coupler</PageTitle>
@@ -9,7 +11,9 @@
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
<button class="btn btn-outline-danger btn-sm logout-button" @onclick="Logout">
<i class="oi oi-account-logout"></i> Logout
</button>
</div>
<article class="content px-4">
@@ -17,3 +21,16 @@
</article>
</main>
</div>
<style>
.logout-button {
float: right;
}
</style>
@code {
private void Logout()
{
AuthService.Logout();
}
}