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.Entities;
using Hospitality.Domain.Enums; using Hospitality.Domain.Enums;
using Hospitality.Infrastructure.Data; using Hospitality.Infrastructure.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace Hospitality.Backend.Data; namespace Hospitality.Backend.Data;
public static class DbSeeder 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 // Check if data already exists
if (context.Events.Any()) if (await context.Events.AnyAsync())
{
return; 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 // Create Events
var summerFestival = new Event var summerFestival = new Event
{ {
@@ -359,6 +374,36 @@ public static class DbSeeder
context.Transactions.AddRange(transactions); 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(); 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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Hospitality.Infrastructure\Hospitality.Infrastructure.csproj" /> <ProjectReference Include="..\Hospitality.Infrastructure\Hospitality.Infrastructure.csproj" />
<ProjectReference Include="..\Hospitality.Domain\Hospitality.Domain.csproj" /> <ProjectReference Include="..\Hospitality.Domain\Hospitality.Domain.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+51 -1
View File
@@ -1,6 +1,12 @@
using System.Text;
using Hospitality.Backend.Configuration;
using Hospitality.Backend.Services; using Hospitality.Backend.Services;
using Hospitality.Domain.Entities;
using Hospitality.Infrastructure.Data; using Hospitality.Infrastructure.Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -12,7 +18,44 @@ builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<HospitalityDbContext>(options => builder.Services.AddDbContext<HospitalityDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); 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 // Register services
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IEventService, EventService>(); builder.Services.AddScoped<IEventService, EventService>();
builder.Services.AddScoped<IGroupService, GroupService>(); builder.Services.AddScoped<IGroupService, GroupService>();
builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IProductService, ProductService>();
@@ -37,7 +80,13 @@ var app = builder.Build();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var context = scope.ServiceProvider.GetRequiredService<HospitalityDbContext>(); 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.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); 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": { "ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=hospitality;Username=postgres;Password=password" "DefaultConnection": "Host=localhost;Database=hospitality;Username=postgres;Password=password"
}, },
"JwtSettings": {
"SecretKey": "ThisIsAVerySecureSecretKeyForDevelopmentOnly123!",
"Issuer": "HospitalityAPI",
"Audience": "HospitalityWeb",
"ExpirationMinutes": 480
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Identity;
namespace Hospitality.Domain.Entities;
public class ApplicationUser : IdentityUser
{
// Optional: Link to Person entity for guest users
public Guid? PersonId { get; set; }
public Person? Person { get; set; }
}
@@ -6,4 +6,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="9.0.0" />
</ItemGroup>
</Project> </Project>
@@ -1,9 +1,10 @@
using Hospitality.Domain.Entities; using Hospitality.Domain.Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Hospitality.Infrastructure.Data; namespace Hospitality.Infrastructure.Data;
public class HospitalityDbContext : DbContext public class HospitalityDbContext : IdentityDbContext<ApplicationUser>
{ {
public HospitalityDbContext(DbContextOptions<HospitalityDbContext> options) : base(options) public HospitalityDbContext(DbContextOptions<HospitalityDbContext> options) : base(options)
{ {
@@ -5,6 +5,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -0,0 +1,587 @@
// <auto-generated />
using System;
using Hospitality.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hospitality.Infrastructure.Migrations
{
[DbContext(typeof(HospitalityDbContext))]
[Migration("20251123211539_AddIdentity")]
partial class AddIdentity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hospitality.Domain.Entities.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<Guid?>("PersonId")
.HasColumnType("uuid");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PersonId");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Hospitality.Domain.Entities.Event", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("EndDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Location")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("StartDate")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Events");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Group", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ContactEmail")
.HasColumnType("text");
b.Property<string>("ContactPersonName")
.HasColumnType("text");
b.Property<Guid>("EventId")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("EventId");
b.ToTable("Groups");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Person", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<Guid>("GroupId")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<Guid>("QrCode")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("GroupId");
b.HasIndex("QrCode")
.IsUnique();
b.ToTable("People");
});
modelBuilder.Entity("Hospitality.Domain.Entities.PersonQuota", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("InitialAmount")
.HasColumnType("integer");
b.Property<Guid>("PersonId")
.HasColumnType("uuid");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<int>("UsedAmount")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("PersonId");
b.HasIndex("ProductId");
b.ToTable("PersonQuotas");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("EventId")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("EventId");
b.ToTable("Products");
});
modelBuilder.Entity("Hospitality.Domain.Entities.QuotaDefinition", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("GroupId")
.HasColumnType("uuid");
b.Property<int>("InitialAmount")
.HasColumnType("integer");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("GroupId");
b.HasIndex("ProductId");
b.ToTable("QuotaDefinitions");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Transaction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("Amount")
.HasColumnType("integer");
b.Property<Guid>("PersonId")
.HasColumnType("uuid");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<Guid?>("StaffId")
.HasColumnType("uuid");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("PersonId");
b.HasIndex("ProductId");
b.ToTable("Transactions");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
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("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
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("text");
b.Property<string>("RoleId")
.HasColumnType("text");
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("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Hospitality.Domain.Entities.ApplicationUser", b =>
{
b.HasOne("Hospitality.Domain.Entities.Person", "Person")
.WithMany()
.HasForeignKey("PersonId");
b.Navigation("Person");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Group", b =>
{
b.HasOne("Hospitality.Domain.Entities.Event", "Event")
.WithMany("Groups")
.HasForeignKey("EventId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Event");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Person", b =>
{
b.HasOne("Hospitality.Domain.Entities.Group", "Group")
.WithMany("People")
.HasForeignKey("GroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Group");
});
modelBuilder.Entity("Hospitality.Domain.Entities.PersonQuota", b =>
{
b.HasOne("Hospitality.Domain.Entities.Person", "Person")
.WithMany("Quotas")
.HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Hospitality.Domain.Entities.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Person");
b.Navigation("Product");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Product", b =>
{
b.HasOne("Hospitality.Domain.Entities.Event", "Event")
.WithMany("Products")
.HasForeignKey("EventId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Event");
});
modelBuilder.Entity("Hospitality.Domain.Entities.QuotaDefinition", b =>
{
b.HasOne("Hospitality.Domain.Entities.Group", "Group")
.WithMany("DefaultQuotas")
.HasForeignKey("GroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Hospitality.Domain.Entities.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Group");
b.Navigation("Product");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Transaction", b =>
{
b.HasOne("Hospitality.Domain.Entities.Person", "Person")
.WithMany()
.HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Hospitality.Domain.Entities.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Person");
b.Navigation("Product");
});
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("Hospitality.Domain.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Hospitality.Domain.Entities.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("Hospitality.Domain.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Hospitality.Domain.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Hospitality.Domain.Entities.Event", b =>
{
b.Navigation("Groups");
b.Navigation("Products");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Group", b =>
{
b.Navigation("DefaultQuotas");
b.Navigation("People");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Person", b =>
{
b.Navigation("Quotas");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,234 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hospitality.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddIdentity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
PersonId = table.Column<Guid>(type: "uuid", nullable: true),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUsers_People_PersonId",
column: x => x.PersonId,
principalTable: "People",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
RoleId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "IX_AspNetUsers_PersonId",
table: "AspNetUsers",
column: "PersonId");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}
@@ -22,6 +22,75 @@ namespace Hospitality.Infrastructure.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hospitality.Domain.Entities.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<Guid?>("PersonId")
.HasColumnType("uuid");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PersonId");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Hospitality.Domain.Entities.Event", b => modelBuilder.Entity("Hospitality.Domain.Entities.Event", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -209,6 +278,147 @@ namespace Hospitality.Infrastructure.Migrations
b.ToTable("Transactions"); b.ToTable("Transactions");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
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("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
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("text");
b.Property<string>("RoleId")
.HasColumnType("text");
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("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Hospitality.Domain.Entities.ApplicationUser", b =>
{
b.HasOne("Hospitality.Domain.Entities.Person", "Person")
.WithMany()
.HasForeignKey("PersonId");
b.Navigation("Person");
});
modelBuilder.Entity("Hospitality.Domain.Entities.Group", b => modelBuilder.Entity("Hospitality.Domain.Entities.Group", b =>
{ {
b.HasOne("Hospitality.Domain.Entities.Event", "Event") b.HasOne("Hospitality.Domain.Entities.Event", "Event")
@@ -299,6 +509,57 @@ namespace Hospitality.Infrastructure.Migrations
b.Navigation("Product"); b.Navigation("Product");
}); });
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("Hospitality.Domain.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Hospitality.Domain.Entities.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("Hospitality.Domain.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Hospitality.Domain.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Hospitality.Domain.Entities.Event", b => modelBuilder.Entity("Hospitality.Domain.Entities.Event", b =>
{ {
b.Navigation("Groups"); b.Navigation("Groups");
+89 -23
View File
@@ -1,6 +1,9 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Link, useNavigate } from 'react-router-dom';
import { Navbar, Nav, Container } from 'react-bootstrap'; import { Navbar, Nav, Container, Button } from 'react-bootstrap';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { LoginPage } from './pages/LoginPage';
import EventsPage from './pages/admin/EventsPage'; import EventsPage from './pages/admin/EventsPage';
import EventDetailPage from './pages/admin/EventDetailPage'; import EventDetailPage from './pages/admin/EventDetailPage';
import GroupDetailPage from './pages/admin/GroupDetailPage'; import GroupDetailPage from './pages/admin/GroupDetailPage';
@@ -9,33 +12,96 @@ import GuestQrPage from './pages/guest/GuestQrPage';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
function NavbarContent() {
const { isAuthenticated, user, logout, hasRole } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/login');
};
return (
<Navbar bg="primary" variant="dark" expand="lg" className="mb-4">
<Container>
<Navbar.Brand as={Link} to="/">Hospitality System</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
{isAuthenticated && (
<>
<Nav className="me-auto">
{hasRole('Admin') && <Nav.Link as={Link} to="/">Admin</Nav.Link>}
{hasRole('Staff') && <Nav.Link as={Link} to="/staff">Staff Scanner</Nav.Link>}
{hasRole('Guest') && <Nav.Link as={Link} to="/guest">Guest View</Nav.Link>}
</Nav>
<Nav>
<Navbar.Text className="me-3">
{user?.email} ({user?.roles.join(', ')})
</Navbar.Text>
<Button variant="outline-light" size="sm" onClick={handleLogout}>
Logout
</Button>
</Nav>
</>
)}
</Navbar.Collapse>
</Container>
</Navbar>
);
}
function App() { function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
<Navbar bg="primary" variant="dark" expand="lg" className="mb-4"> <AuthProvider>
<NavbarContent />
<Container> <Container>
<Navbar.Brand as={Link} to="/">Hospitality System</Navbar.Brand> <Routes>
<Navbar.Toggle aria-controls="basic-navbar-nav" /> <Route path="/login" element={<LoginPage />} />
<Navbar.Collapse id="basic-navbar-nav"> <Route
<Nav className="ms-auto"> path="/"
<Nav.Link as={Link} to="/">Admin</Nav.Link> element={
<Nav.Link as={Link} to="/staff">Staff Scanner</Nav.Link> <ProtectedRoute roles={['Admin']}>
<Nav.Link as={Link} to="/guest">Guest View</Nav.Link> <EventsPage />
</Nav> </ProtectedRoute>
</Navbar.Collapse> }
/>
<Route
path="/events/:id"
element={
<ProtectedRoute roles={['Admin']}>
<EventDetailPage />
</ProtectedRoute>
}
/>
<Route
path="/groups/:id"
element={
<ProtectedRoute roles={['Admin']}>
<GroupDetailPage />
</ProtectedRoute>
}
/>
<Route
path="/staff"
element={
<ProtectedRoute roles={['Staff']}>
<ScannerPage />
</ProtectedRoute>
}
/>
<Route
path="/guest"
element={
<ProtectedRoute roles={['Guest']}>
<GuestQrPage />
</ProtectedRoute>
}
/>
</Routes>
</Container> </Container>
</Navbar> </AuthProvider>
<Container>
<Routes>
<Route path="/" element={<EventsPage />} />
<Route path="/events/:id" element={<EventDetailPage />} />
<Route path="/groups/:id" element={<GroupDetailPage />} />
<Route path="/staff" element={<ScannerPage />} />
<Route path="/guest" element={<GuestQrPage />} />
</Routes>
</Container>
</BrowserRouter> </BrowserRouter>
</QueryClientProvider> </QueryClientProvider>
); );
@@ -0,0 +1,25 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
roles?: string[];
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children, roles }) => {
const { isAuthenticated, hasRole } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (roles && roles.length > 0) {
const hasRequiredRole = roles.some((role) => hasRole(role));
if (!hasRequiredRole) {
return <Navigate to="/login" replace />;
}
}
return <>{children}</>;
};
@@ -0,0 +1,76 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { authApi, LoginRequest, UserInfo } from '../lib/api';
interface AuthContextType {
user: UserInfo | null;
token: string | null;
login: (data: LoginRequest) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
hasRole: (role: string) => boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<UserInfo | null>(null);
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
// Load from localStorage on mount
const storedToken = localStorage.getItem('token');
const storedUser = localStorage.getItem('user');
if (storedToken && storedUser) {
setToken(storedToken);
setUser(JSON.parse(storedUser));
}
}, []);
const login = async (data: LoginRequest) => {
const response = await authApi.login(data);
const userInfo: UserInfo = {
email: response.email,
roles: response.roles,
};
setToken(response.token);
setUser(userInfo);
localStorage.setItem('token', response.token);
localStorage.setItem('user', JSON.stringify(userInfo));
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem('token');
localStorage.removeItem('user');
};
const hasRole = (role: string) => {
return user?.roles.includes(role) ?? false;
};
return (
<AuthContext.Provider
value={{
user,
token,
login,
logout,
isAuthenticated: !!token,
hasRole,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
+48 -42
View File
@@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5163/api'; const API_BASE_URL = 'http://localhost:5163/api';
export const api = axios.create({ export const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
@@ -9,50 +9,56 @@ export const api = axios.create({
}, },
}); });
// Event API // Request interceptor to add JWT token
export const eventsApi = { api.interceptors.request.use(
getAll: () => api.get('/events'), (config) => {
getById: (id: string) => api.get(`/events/${id}`), const token = localStorage.getItem('token');
create: (data: any) => api.post('/events', data), if (token) {
update: (id: string, data: any) => api.put(`/events/${id}`, data), config.headers.Authorization = `Bearer ${token}`;
delete: (id: string) => api.delete(`/events/${id}`), }
}; return config;
},
(error) => Promise.reject(error)
);
// Group API // Response interceptor for 401 errors
export const groupsApi = { api.interceptors.response.use(
getByEventId: (eventId: string) => api.get(`/events/${eventId}/groups`), (response) => response,
getById: (id: string) => api.get(`/groups/${id}`), (error) => {
create: (eventId: string, data: any) => api.post(`/events/${eventId}/groups`, data), if (error.response?.status === 401) {
update: (id: string, data: any) => api.put(`/groups/${id}`, data), localStorage.removeItem('token');
delete: (id: string) => api.delete(`/groups/${id}`), localStorage.removeItem('user');
}; window.location.href = '/login';
}
return Promise.reject(error);
}
);
// People API // Auth API
export const peopleApi = { export interface LoginRequest {
getById: (id: string) => api.get(`/people/${id}`), email: string;
create: (groupId: string, data: any) => api.post(`/groups/${groupId}/people`, data), password: string;
update: (id: string, data: any) => api.put(`/people/${id}`, data), }
delete: (id: string) => api.delete(`/people/${id}`),
assignQuota: (personId: string, data: any) => api.post(`/people/${personId}/quotas`, data),
};
// Product API export interface LoginResponse {
export const productsApi = { token: string;
getByEventId: (eventId: string) => api.get(`/events/${eventId}/products`), email: string;
create: (eventId: string, data: any) => api.post(`/events/${eventId}/products`, data), roles: string[];
update: (id: string, data: any) => api.put(`/products/${id}`, data), }
delete: (id: string) => api.delete(`/products/${id}`),
};
// QR Code API export interface UserInfo {
export const qrCodeApi = { email: string;
getByQrCode: (qrCode: string) => api.get(`/qr/${qrCode}`), roles: string[];
getQuotas: (qrCode: string) => api.get(`/qr/${qrCode}/quotas`), }
};
// Transaction API export const authApi = {
export const transactionsApi = { login: async (data: LoginRequest): Promise<LoginResponse> => {
create: (data: any) => api.post('/transactions', data), const response = await axios.post(`${API_BASE_URL}/auth/login`, data);
getByPersonId: (personId: string) => api.get(`/transactions/person/${personId}`), return response.data;
getByEventId: (eventId: string) => api.get(`/transactions/event/${eventId}`), },
me: async (): Promise<UserInfo> => {
const response = await api.get('/auth/me');
return response.data;
},
}; };
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Container, Card, Form, Button, Alert } from 'react-bootstrap';
import { useAuth } from '../contexts/AuthContext';
export const LoginPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { login } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login({ email, password });
navigate('/');
} catch (err) {
setError('Invalid email or password');
} finally {
setLoading(false);
}
};
return (
<Container className="d-flex align-items-center justify-content-center" style={{ minHeight: '100vh' }}>
<Card style={{ maxWidth: '400px', width: '100%' }} className="shadow">
<Card.Body className="p-4">
<h2 className="text-center mb-4">Login</h2>
{error && <Alert variant="danger">{error}</Alert>}
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="admin@example.com"
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="Enter password"
/>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100"
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</Button>
</Form>
<div className="mt-4 text-center">
<small className="text-muted">
<strong>Test Accounts:</strong><br />
Admin: admin@example.com / Admin123!<br />
Staff: staff@example.com / Staff123!<br />
Guest: guest@example.com / Guest123!
</small>
</div>
</Card.Body>
</Card>
</Container>
);
};