Add JWT authentication and role-based authorization

This commit is contained in:
steinhelge
2025-11-23 22:26:17 +01:00
parent 587847e7ed
commit 20af7d5b52
21 changed files with 1726 additions and 77 deletions
@@ -0,0 +1,9 @@
namespace Hospitality.Backend.Configuration;
public class JwtSettings
{
public string SecretKey { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; } = 60;
}
@@ -0,0 +1,55 @@
using Hospitality.Backend.DTOs;
using Hospitality.Backend.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace Hospitality.Backend.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest request)
{
var response = await _authService.LoginAsync(request);
if (response == null)
return Unauthorized(new { message = "Invalid email or password" });
return Ok(response);
}
[HttpPost("register")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult> Register([FromBody] RegisterRequest request)
{
var success = await _authService.RegisterAsync(request);
if (!success)
return BadRequest(new { message = "Registration failed" });
return Ok(new { message = "User registered successfully" });
}
[HttpGet("me")]
[Authorize]
public async Task<ActionResult<UserInfoResponse>> GetCurrentUser()
{
var email = User.FindFirstValue(ClaimTypes.Email);
if (email == null)
return Unauthorized();
var userInfo = await _authService.GetUserInfoAsync(email);
if (userInfo == null)
return NotFound();
return Ok(userInfo);
}
}
+23
View File
@@ -0,0 +1,23 @@
namespace Hospitality.Backend.DTOs;
public record LoginRequest(
string Email,
string Password
);
public record LoginResponse(
string Token,
string Email,
string[] Roles
);
public record RegisterRequest(
string Email,
string Password,
string Role
);
public record UserInfoResponse(
string Email,
string[] Roles
);
+49 -4
View File
@@ -1,19 +1,34 @@
using Hospitality.Domain.Entities;
using Hospitality.Domain.Enums;
using Hospitality.Infrastructure.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace Hospitality.Backend.Data;
public static class DbSeeder
{
public static async Task SeedAsync(HospitalityDbContext context)
public static async Task SeedAsync(HospitalityDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
// Check if already seeded
if (context.Events.Any())
{
// Check if data already exists
if (await context.Events.AnyAsync())
return;
// Seed Roles
var roles = new[] { "Admin", "Staff", "Guest" };
foreach (var roleName in roles)
{
if (!await roleManager.RoleExistsAsync(roleName))
{
await roleManager.CreateAsync(new IdentityRole(roleName));
}
}
// Seed Users
await SeedUserAsync(userManager, "admin@example.com", "Admin123!", "Admin");
await SeedUserAsync(userManager, "staff@example.com", "Staff123!", "Staff");
var guestUser = await SeedUserAsync(userManager, "guest@example.com", "Guest123!", "Guest");
// Create Events
var summerFestival = new Event
{
@@ -359,6 +374,36 @@ public static class DbSeeder
context.Transactions.AddRange(transactions);
// Link guest user to John Doe (for testing guest view)
if (guestUser != null)
{
guestUser.PersonId = johnDoe.Id;
await userManager.UpdateAsync(guestUser);
}
await context.SaveChangesAsync();
}
private static async Task<ApplicationUser?> SeedUserAsync(UserManager<ApplicationUser> userManager, string email, string password, string role)
{
var existingUser = await userManager.FindByEmailAsync(email);
if (existingUser != null)
return existingUser;
var user = new ApplicationUser
{
UserName = email,
Email = email,
EmailConfirmed = true
};
var result = await userManager.CreateAsync(user, password);
if (result.Succeeded)
{
await userManager.AddToRoleAsync(user, role);
return user;
}
return null;
}
}
@@ -7,17 +7,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hospitality.Infrastructure\Hospitality.Infrastructure.csproj" />
<ProjectReference Include="..\Hospitality.Domain\Hospitality.Domain.csproj" />
<ItemGroup>
<ProjectReference Include="..\Hospitality.Infrastructure\Hospitality.Infrastructure.csproj" />
<ProjectReference Include="..\Hospitality.Domain\Hospitality.Domain.csproj" />
</ItemGroup>
</Project>
+51 -1
View File
@@ -1,6 +1,12 @@
using System.Text;
using Hospitality.Backend.Configuration;
using Hospitality.Backend.Services;
using Hospitality.Domain.Entities;
using Hospitality.Infrastructure.Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
@@ -12,7 +18,44 @@ builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<HospitalityDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Configure JWT settings
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings"));
var jwtSettings = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>();
// Add Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<HospitalityDbContext>()
.AddDefaultTokenProviders();
// Add JWT Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings!.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey))
};
});
// Register services
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IEventService, EventService>();
builder.Services.AddScoped<IGroupService, GroupService>();
builder.Services.AddScoped<IProductService, ProductService>();
@@ -37,7 +80,13 @@ var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<HospitalityDbContext>();
await Hospitality.Backend.Data.DbSeeder.SeedAsync(context);
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
// Apply migrations
await context.Database.MigrateAsync();
await Hospitality.Backend.Data.DbSeeder.SeedAsync(context, userManager, roleManager);
}
@@ -52,6 +101,7 @@ app.UseHttpsRedirection();
app.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
@@ -0,0 +1,96 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Hospitality.Backend.Configuration;
using Hospitality.Backend.DTOs;
using Hospitality.Domain.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Hospitality.Backend.Services;
public class AuthService : IAuthService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly JwtSettings _jwtSettings;
public AuthService(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IOptions<JwtSettings> jwtSettings)
{
_userManager = userManager;
_signInManager = signInManager;
_jwtSettings = jwtSettings.Value;
}
public async Task<LoginResponse?> LoginAsync(LoginRequest request)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null)
return null;
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!result.Succeeded)
return null;
var roles = await _userManager.GetRolesAsync(user);
var token = GenerateJwtToken(user, roles.ToArray());
return new LoginResponse(token, user.Email!, roles.ToArray());
}
public async Task<bool> RegisterAsync(RegisterRequest request)
{
var user = new ApplicationUser
{
UserName = request.Email,
Email = request.Email
};
var result = await _userManager.CreateAsync(user, request.Password);
if (!result.Succeeded)
return false;
await _userManager.AddToRoleAsync(user, request.Role);
return true;
}
public async Task<UserInfoResponse?> GetUserInfoAsync(string email)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
return null;
var roles = await _userManager.GetRolesAsync(user);
return new UserInfoResponse(user.Email!, roles.ToArray());
}
private string GenerateJwtToken(ApplicationUser user, string[] roles)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email!),
new(JwtRegisteredClaimNames.Sub, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
@@ -0,0 +1,10 @@
using Hospitality.Backend.DTOs;
namespace Hospitality.Backend.Services;
public interface IAuthService
{
Task<LoginResponse?> LoginAsync(LoginRequest request);
Task<bool> RegisterAsync(RegisterRequest request);
Task<UserInfoResponse?> GetUserInfoAsync(string email);
}
+6
View File
@@ -2,6 +2,12 @@
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=hospitality;Username=postgres;Password=password"
},
"JwtSettings": {
"SecretKey": "ThisIsAVerySecureSecretKeyForDevelopmentOnly123!",
"Issuer": "HospitalityAPI",
"Audience": "HospitalityWeb",
"ExpirationMinutes": 480
},
"Logging": {
"LogLevel": {
"Default": "Information",