Add JWT authentication and role-based authorization
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user