Creata pagina login con Syncfusion

This commit is contained in:
2025-11-27 22:23:17 +01:00
parent e528927d96
commit 9369b70e2d
16 changed files with 1389 additions and 142 deletions

9
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,9 @@
- Always use controllers for API endpoints
- Always use dependency injection
- Use the repository pattern
- Add XML summary comments
- Use DTOs where it makes sense
- Use separate files for each class, interface, and DTO
- Always show a plan first before writing code
- Follow best practices and clean code principles
- When refered to Syncfusion Blazor components , always use syncfusion-blazor-assistant mcp

View File

@@ -4,125 +4,350 @@
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using SmartDB.Data
@using Syncfusion.Blazor.Inputs
@using Syncfusion.Blazor.Buttons
@inject SignInManager<ApplicationUser> SignInManager
@inject ILogger<Login> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Log in</PageTitle>
<PageTitle>Accedi - SmartDB</PageTitle>
<h1>Log in</h1>
<div class="row">
<div class="col-md-4">
<section>
<StatusMessage Message="@errorMessage" />
<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login">
<div class="login-container">
<style>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.login-wrapper {
width: 100%;
max-width: 420px;
padding: 2rem;
}
.login-card {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 3rem 2.5rem;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-logo {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #667eea;
font-weight: 700;
}
.login-title {
font-size: 1.75rem;
color: #333;
margin-bottom: 0.5rem;
font-weight: 600;
}
.login-subtitle {
font-size: 0.95rem;
color: #888;
margin-bottom: 2rem;
}
.form-group-custom {
margin-bottom: 1.5rem;
}
.form-group-custom label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
font-size: 0.9rem;
}
::deep .e-input-group {
width: 100%;
}
::deep .e-input-group input.e-field {
border: 2px solid #e0e0e0 !important;
border-radius: 8px !important;
padding: 0.75rem 1rem !important;
font-size: 0.95rem !important;
transition: all 0.3s ease !important;
}
::deep .e-input-group input.e-field:focus {
border-color: #667eea !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
}
::deep .e-checkbox-wrapper {
margin-top: 0.5rem;
}
::deep .e-checkbox-wrapper label {
color: #666 !important;
font-size: 0.9rem !important;
}
.login-button {
width: 100%;
margin-top: 2rem;
margin-bottom: 1.5rem;
}
::deep .e-btn {
border-radius: 8px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
transition: all 0.3s ease !important;
border: none !important;
width: 100% !important;
}
::deep .e-btn.e-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
padding: 0.75rem 1.5rem !important;
font-size: 0.95rem !important;
min-height: 48px !important;
color: white !important;
}
::deep .e-btn.e-primary:hover:not(:disabled) {
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4) !important;
transform: translateY(-2px) !important;
}
::deep .e-btn.e-primary:disabled {
opacity: 0.7 !important;
cursor: not-allowed !important;
}
.login-links {
text-align: center;
margin-top: 1.5rem;
}
.login-links a {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
transition: color 0.3s ease;
}
.login-links a:hover {
color: #764ba2;
text-decoration: underline;
}
.alert-custom {
border-radius: 8px;
margin-bottom: 1.5rem;
border: none;
padding: 1rem;
}
.alert-danger-custom {
background-color: #fee;
color: #c33;
border-left: 4px solid #c33;
}
.validation-error {
color: #d32f2f;
font-size: 0.8rem;
margin-top: 0.3rem;
}
.spinner-custom {
display: inline-block;
width: 16px;
height: 16px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-right: 0.5rem;
}
@@keyframes spin {
to { transform: rotate(360deg); }
}
.login-divider {
display: flex;
align-items: center;
margin: 2rem 0;
color: #ccc;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background-color: #ccc;
}
.login-divider span {
padding: 0 1rem;
font-size: 0.85rem;
color: #999;
}
</style>
<div class="login-wrapper">
<div class="login-card">
<div class="login-header">
<div class="login-logo">🔐</div>
<h1 class="login-title">SmartDB</h1>
<p class="login-subtitle">Accedi al tuo account</p>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-custom alert-danger-custom">
<strong>Errore!</strong> @errorMessage
</div>
}
<EditForm Model="Input" FormName="login" OnValidSubmit="LoginUser">
<DataAnnotationsValidator />
<h2>Use a local account to log in.</h2>
<hr />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
<label for="email" class="form-label">Email</label>
<ValidationMessage For="() => Input.Email" class="text-danger" />
<div class="form-group-custom">
<label for="email">Email</label>
<SfTextBox @bind-Value="Input.Email"
id="email"
Placeholder="name@example.com"
CssClass="form-control-sf"
ShowClearButton="true">
</SfTextBox>
<ValidationMessage For="() => Input.Email" class="validation-error" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
<label for="password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
<div class="form-group-custom">
<label for="password">Password</label>
<SfTextBox @bind-Value="Input.Password"
id="password"
Placeholder="Inserisci la tua password"
CssClass="form-control-sf"
TextMode="InputType.Password"
ShowClearButton="true">
</SfTextBox>
<ValidationMessage For="() => Input.Password" class="validation-error" />
</div>
<div class="checkbox mb-3">
<label class="form-label">
<InputCheckbox @bind-Value="Input.RememberMe" class="darker-border-checkbox form-check-input" />
Remember me
</label>
<div class="form-group-custom">
<SfCheckBox @bind-Checked="Input.RememberMe"
Label="Ricordami"
CssClass="remember-me-checkbox">
</SfCheckBox>
</div>
<div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
<div class="login-button">
<SfButton IconCss="e-icon-search"
IsPrimary="true"
Disabled="@isSubmitting"
OnClick="async () => await LoginUser()">
@if (isSubmitting)
{
<span class="spinner-custom"></span>
<span>Accesso in corso...</span>
}
else
{
<span>Accedi</span>
}
</SfButton>
</div>
<div>
<div class="login-divider">
<span>Supporto</span>
</div>
<div class="login-links">
<p>
<a href="Account/ForgotPassword">Forgot your password?</a>
</p>
<p>
<a href="@(NavigationManager.GetUriWithQueryParameters("Account/Register", new Dictionary<string, object?> { ["ReturnUrl"] = ReturnUrl }))">Register as a new user</a>
</p>
<p>
<a href="Account/ResendEmailConfirmation">Resend email confirmation</a>
<a href="Account/ForgotPassword">Password dimenticata?</a>
</p>
</div>
</EditForm>
</section>
</div>
<div class="col-md-6 col-md-offset-2">
<section>
<h3>Use another service to log in.</h3>
<hr />
<ExternalLoginPicker />
</section>
</div>
</div>
</div>
@code {
private string? errorMessage;
private bool isSubmitting = false;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
}
}
public async Task LoginUser()
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
isSubmitting = true;
errorMessage = null;
try
{
Logger.LogInformation("User logged in.");
RedirectManager.RedirectTo(ReturnUrl);
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
RedirectManager.RedirectTo("/");
}
else if (result.IsLockedOut)
{
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
errorMessage = "Email o password non validi. Riprova.";
}
}
else if (result.RequiresTwoFactor)
catch (Exception ex)
{
RedirectManager.RedirectTo(
"Account/LoginWith2fa",
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
Logger.LogError(ex, "Errore durante il login");
errorMessage = "Errore durante l'accesso. Riprova più tardi.";
}
else if (result.IsLockedOut)
finally
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
errorMessage = "Error: Invalid login attempt.";
isSubmitting = false;
}
}
private sealed class InputModel
private class InputModel
{
[Required]
[EmailAddress]
[Required(ErrorMessage = "L'email è obbligatoria")]
[EmailAddress(ErrorMessage = "Inserisci un'email valida")]
public string Email { get; set; } = "";
[Required]
[Required(ErrorMessage = "La password è obbligatoria")]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[Display(Name = "Remember me?")]
[Display(Name = "Ricordami?")]
public bool RememberMe { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
using System.ComponentModel.DataAnnotations;
namespace SmartDB.Components.Admin.Dtos
{
/// <summary>
/// DTO per la creazione di un nuovo utente da parte dell'amministratore
/// </summary>
public class CreateUserDto
{
/// <summary>
/// Email dell'utente
/// </summary>
[Required(ErrorMessage = "L'email <20> obbligatoria")]
[EmailAddress(ErrorMessage = "Inserisci un'email valida")]
public string Email { get; set; } = string.Empty;
/// <summary>
/// Nome dell'utente
/// </summary>
[Required(ErrorMessage = "Il nome <20> obbligatorio")]
public string FirstName { get; set; } = string.Empty;
/// <summary>
/// Cognome dell'utente
/// </summary>
[Required(ErrorMessage = "Il cognome <20> obbligatorio")]
public string LastName { get; set; } = string.Empty;
/// <summary>
/// Password temporanea per l'utente
/// </summary>
[Required(ErrorMessage = "La password <20> obbligatoria")]
[StringLength(100, MinimumLength = 6, ErrorMessage = "La password deve avere almeno 6 caratteri")]
public string Password { get; set; } = string.Empty;
/// <summary>
/// Ruolo da assegnare all'utente
/// </summary>
public string Role { get; set; } = "User";
}
}

View File

@@ -0,0 +1,265 @@
@page "/admin/users"
@using Microsoft.AspNetCore.Authorization
@using SmartDB.Components.Admin.Dtos
@using SmartDB.Components.Admin.Services
@using SmartDB.Data
@attribute [Authorize(Policy = "AdminOnly")]
@inject IUserManagementService UserService
@inject ILogger<Users> Logger
<PageTitle>Gestione Utenti - Admin</PageTitle>
<h1>Gestione Utenti</h1>
@if (!string.IsNullOrEmpty(successMessage))
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@successMessage
<button type="button" class="btn-close" @onclick="@(() => successMessage = string.Empty)" aria-label="Close"></button>
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@errorMessage
<button type="button" class="btn-close" @onclick="@(() => errorMessage = string.Empty)" aria-label="Close"></button>
</div>
}
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "list" ? "active" : "")" @onclick="@(() => activeTab = "list")" type="button" role="tab" aria-selected="@(activeTab == "list")">
Elenco Utenti
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "create" ? "active" : "")" @onclick="@(() => activeTab = "create")" type="button" role="tab" aria-selected="@(activeTab == "create")">
Aggiungi Utente
</button>
</li>
</ul>
<div class="tab-content">
@if (activeTab == "list")
{
<div class="tab-pane fade show active">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Email</th>
<th>Nome</th>
<th>Cognome</th>
<th>Ruolo</th>
<th>Stato</th>
<th>Data Creazione</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
@if (users != null && users.Any())
{
@foreach (var user in users)
{
<tr>
<td>@user.Email</td>
<td>@user.FirstName</td>
<td>@user.LastName</td>
<td>
<span class="badge bg-info">@GetUserRole(user.Id)</span>
</td>
<td>
@if (user.IsActive)
{
<span class="badge bg-success">Attivo</span>
}
else
{
<span class="badge bg-danger">Disattivo</span>
}
</td>
<td>@user.CreatedAt.ToShortDateString()</td>
<td>
<button class="btn btn-sm btn-warning" @onclick="@(() => ToggleUserStatus(user.Id))" title="Cambia stato">
@(user.IsActive ? "Disabilita" : "Abilita")
</button>
<button class="btn btn-sm btn-danger" @onclick="@(() => DeleteUser(user.Id))" title="Elimina">
Elimina
</button>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="7" class="text-center">Nessun utente trovato</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
@if (activeTab == "create")
{
<div class="tab-pane fade show active">
<div class="row">
<div class="col-md-6">
<h3>Aggiungi Nuovo Utente</h3>
<EditForm Model="newUser" OnValidSubmit="HandleCreateUser">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" />
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<InputText id="email" class="form-control" @bind-Value="newUser.Email" placeholder="utente@example.com" />
<ValidationMessage For="@(() => newUser.Email)" />
</div>
<div class="mb-3">
<label for="firstName" class="form-label">Nome</label>
<InputText id="firstName" class="form-control" @bind-Value="newUser.FirstName" placeholder="Mario" />
<ValidationMessage For="@(() => newUser.FirstName)" />
</div>
<div class="mb-3">
<label for="lastName" class="form-label">Cognome</label>
<InputText id="lastName" class="form-control" @bind-Value="newUser.LastName" placeholder="Rossi" />
<ValidationMessage For="@(() => newUser.LastName)" />
</div>
<div class="mb-3">
<label for="password" class="form-label">Password Temporanea</label>
<InputText type="password" id="password" class="form-control" @bind-Value="newUser.Password" placeholder="Min 6 caratteri" />
<ValidationMessage For="@(() => newUser.Password)" />
</div>
<div class="mb-3">
<label for="role" class="form-label">Ruolo</label>
<InputSelect id="role" class="form-control" @bind-Value="newUser.Role">
<option value="User">Utente</option>
<option value="Admin">Amministratore</option>
</InputSelect>
</div>
<button type="submit" class="btn btn-primary" disabled="@isSubmitting">
@if (isSubmitting)
{
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span>Creazione in corso...</span>
}
else
{
<span>Crea Utente</span>
}
</button>
</EditForm>
</div>
</div>
</div>
}
</div>
@code {
private string activeTab = "list";
private List<ApplicationUser>? users;
private CreateUserDto newUser = new();
private string successMessage = string.Empty;
private string errorMessage = string.Empty;
private bool isSubmitting = false;
private Dictionary<string, string> userRoles = new();
protected override async Task OnInitializedAsync()
{
await LoadUsers();
}
private async Task LoadUsers()
{
try
{
users = await UserService.GetAllUsersAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento degli utenti");
errorMessage = "Errore nel caricamento degli utenti";
}
}
private async Task HandleCreateUser()
{
isSubmitting = true;
try
{
var (success, message) = await UserService.CreateUserAsync(newUser);
if (success)
{
successMessage = message;
newUser = new();
await LoadUsers();
activeTab = "list";
}
else
{
errorMessage = message;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella creazione dell'utente");
errorMessage = "Errore nella creazione dell'utente";
}
finally
{
isSubmitting = false;
}
}
private async Task DeleteUser(string userId)
{
if (await JsConfirm("Sei sicuro di voler eliminare questo utente?"))
{
var (success, message) = await UserService.DeleteUserAsync(userId);
if (success)
{
successMessage = message;
await LoadUsers();
}
else
{
errorMessage = message;
}
}
}
private async Task ToggleUserStatus(string userId)
{
var (success, message) = await UserService.ToggleUserStatusAsync(userId);
if (success)
{
successMessage = message;
await LoadUsers();
}
else
{
errorMessage = message;
}
}
private string GetUserRole(string userId)
{
// Per ora returniamo "User" - in futuro implementeremo la logica per recuperare i ruoli
return "User";
}
private async Task<bool> JsConfirm(string message)
{
// Placeholder - in un componente reale userebbero JS interop
return true;
}
}

View File

@@ -0,0 +1,155 @@
using Microsoft.AspNetCore.Identity;
using SmartDB.Components.Admin.Dtos;
using SmartDB.Data;
namespace SmartDB.Components.Admin.Services
{
/// <summary>
/// Servizio per la gestione degli utenti dell'applicazione
/// </summary>
public interface IUserManagementService
{
/// <summary>
/// Crea un nuovo utente con il ruolo specificato
/// </summary>
Task<(bool Success, string Message)> CreateUserAsync(CreateUserDto dto);
/// <summary>
/// Recupera tutti gli utenti
/// </summary>
Task<List<ApplicationUser>> GetAllUsersAsync();
/// <summary>
/// Elimina un utente
/// </summary>
Task<(bool Success, string Message)> DeleteUserAsync(string userId);
/// <summary>
/// Disabilita/abilita un utente
/// </summary>
Task<(bool Success, string Message)> ToggleUserStatusAsync(string userId);
}
/// <summary>
/// Implementazione del servizio di gestione degli utenti
/// </summary>
public class UserManagementService : IUserManagementService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public UserManagementService(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
public async Task<(bool Success, string Message)> CreateUserAsync(CreateUserDto dto)
{
try
{
// Verifica se l'email <20> gi<67> in uso
var existingUser = await _userManager.FindByEmailAsync(dto.Email);
if (existingUser != null)
{
return (false, "Un utente con questa email esiste gi<67>");
}
// Crea il nuovo utente
var user = new ApplicationUser
{
UserName = dto.Email,
Email = dto.Email,
FirstName = dto.FirstName,
LastName = dto.LastName,
EmailConfirmed = true,
IsActive = true
};
var result = await _userManager.CreateAsync(user, dto.Password);
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
return (false, $"Errore nella creazione dell'utente: {errors}");
}
// Assegna il ruolo
var roleExist = await _roleManager.RoleExistsAsync(dto.Role);
if (!roleExist)
{
dto.Role = "User"; // Fallback al ruolo User
}
var roleResult = await _userManager.AddToRoleAsync(user, dto.Role);
if (!roleResult.Succeeded)
{
var errors = string.Join(", ", roleResult.Errors.Select(e => e.Description));
return (false, $"Errore nell'assegnazione del ruolo: {errors}");
}
return (true, "Utente creato con successo");
}
catch (Exception ex)
{
return (false, $"Errore: {ex.Message}");
}
}
public async Task<List<ApplicationUser>> GetAllUsersAsync()
{
return _userManager.Users.ToList();
}
public async Task<(bool Success, string Message)> DeleteUserAsync(string userId)
{
try
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return (false, "Utente non trovato");
}
var result = await _userManager.DeleteAsync(user);
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
return (false, $"Errore nell'eliminazione: {errors}");
}
return (true, "Utente eliminato con successo");
}
catch (Exception ex)
{
return (false, $"Errore: {ex.Message}");
}
}
public async Task<(bool Success, string Message)> ToggleUserStatusAsync(string userId)
{
try
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return (false, "Utente non trovato");
}
user.IsActive = !user.IsActive;
var result = await _userManager.UpdateAsync(user);
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
return (false, $"Errore nell'aggiornamento: {errors}");
}
return (true, $"Utente {(user.IsActive ? "abilitato" : "disabilitato")} con successo");
}
catch (Exception ex)
{
return (false, $"Errore: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using SmartDB
@using SmartDB.Components
@using SmartDB.Data

View File

@@ -7,6 +7,7 @@
<base href="/" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="SmartDB.styles.css" />
<link href="_content/Syncfusion.Blazor.Core/styles/bootstrap5.css" rel="stylesheet" />
<HeadOutlet />
</head>

View File

@@ -1,7 +1,67 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
@using System.Security.Claims
@attribute [Authorize]
<PageTitle>Home</PageTitle>
@inject AuthenticationStateProvider AuthenticationStateProvider
<h1>Hello, world!</h1>
<PageTitle>Home - SmartDB</PageTitle>
Welcome to your new app.
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-8">
<h1>Benvenuto in SmartDB</h1>
<p class="lead">Applicazione di gestione avanzata</p>
</div>
<div class="col-md-4 text-end">
<div class="card">
<div class="card-body">
<p class="card-text">Utente: <strong>@currentUserEmail</strong></p>
<p class="card-text text-muted">@currentUserName</p>
</div>
</div>
</div>
</div>
@if (isAdmin)
{
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">Pannello Amministratore</h4>
<p>Hai accesso alle funzioni di amministrazione.</p>
<hr />
<a href="/admin/users" class="btn btn-sm btn-primary">Gestisci Utenti</a>
</div>
}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5>Dashboard</h5>
</div>
<div class="card-body">
<p>Benvenuto nell'applicazione SmartDB. Qui troverai tutti gli strumenti necessari per gestire i tuoi dati.</p>
</div>
</div>
</div>
</div>
</div>
@code {
private string currentUserEmail = string.Empty;
private string currentUserName = string.Empty;
private bool isAdmin = false;
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated ?? false)
{
currentUserEmail = user.FindFirst(ClaimTypes.Email)?.Value ?? user.Identity.Name ?? "Utente";
currentUserName = $"{user.FindFirst(ClaimTypes.GivenName)?.Value} {user.FindFirst(ClaimTypes.Surname)?.Value}".Trim();
isAdmin = user.IsInRole("Admin");
}
}
}

View File

@@ -9,3 +9,4 @@
@using Microsoft.JSInterop
@using SmartDB
@using SmartDB.Components
@using Syncfusion.Blazor

View File

@@ -1,9 +1,39 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace SmartDB.Data
{
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options)
/// <summary>
/// Context di Entity Framework per l'applicazione SmartDB
/// </summary>
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser, IdentityRole, string>(options)
{
/// <summary>
/// Configura il modello del database
/// </summary>
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Seed dei ruoli di sistema
var adminRoleId = "1";
var userRoleId = "2";
builder.Entity<IdentityRole>().HasData(
new IdentityRole
{
Id = adminRoleId,
Name = "Admin",
NormalizedName = "ADMIN"
},
new IdentityRole
{
Id = userRoleId,
Name = "User",
NormalizedName = "USER"
}
);
}
}
}

View File

@@ -2,9 +2,29 @@ using Microsoft.AspNetCore.Identity;
namespace SmartDB.Data
{
// Add profile data for application users by adding properties to the ApplicationUser class
/// <summary>
/// Estende IdentityUser con propriet<65> aggiuntive per il profilo dell'applicazione
/// </summary>
public class ApplicationUser : IdentityUser
{
}
/// <summary>
/// Nome dell'utente
/// </summary>
public string? FirstName { get; set; }
/// <summary>
/// Cognome dell'utente
/// </summary>
public string? LastName { get; set; }
/// <summary>
/// Indica se l'utente <20> attivo
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Data di creazione dell'utente
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,305 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SmartDB.Data;
#nullable disable
namespace SmartDB.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251127211042_AddUserPropertiesAndRoles")]
partial class AddUserPropertiesAndRoles
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.22")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
b.HasData(
new
{
Id = "1",
Name = "Admin",
NormalizedName = "ADMIN"
},
new
{
Id = "2",
Name = "User",
NormalizedName = "USER"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("SmartDB.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("LastName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("SmartDB.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("SmartDB.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SmartDB.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("SmartDB.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace SmartDB.Migrations
{
/// <inheritdoc />
public partial class AddUserPropertiesAndRoles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "AspNetUsers",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<string>(
name: "FirstName",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsActive",
table: "AspNetUsers",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "LastName",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
values: new object[,]
{
{ "1", null, "Admin", "ADMIN" },
{ "2", null, "User", "USER" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: "1");
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: "2");
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "FirstName",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "IsActive",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "LastName",
table: "AspNetUsers");
}
}
}

View File

@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -17,76 +17,11 @@ namespace SmartDB.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("ProductVersion", "8.0.22")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("SmartDB.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
@@ -112,6 +47,20 @@ namespace SmartDB.Migrations
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
b.HasData(
new
{
Id = "1",
Name = "Admin",
NormalizedName = "ADMIN"
},
new
{
Id = "2",
Name = "User",
NormalizedName = "USER"
});
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
@@ -220,6 +169,83 @@ namespace SmartDB.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("SmartDB.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("LastName")
.HasColumnType("nvarchar(max)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)

View File

@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SmartDB.Components;
using SmartDB.Components.Account;
using SmartDB.Components.Admin.Services;
using SmartDB.Data;
using Syncfusion.Blazor;
var builder = WebApplication.CreateBuilder(args);
@@ -11,11 +14,17 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Registra Syncfusion Blazor
builder.Services.AddSyncfusionBlazor();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
// Aggiungi servizi di gestione
builder.Services.AddScoped<IUserManagementService, UserManagementService>();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
@@ -28,13 +37,19 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
// Aggiungi Authorization
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
var app = builder.Build();
// Configure the HTTP request pipeline.

View File

@@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.22" />
<PackageReference Include="Syncfusion.Blazor" Version="28.1.33" />
</ItemGroup>
</Project>