Add JWT authentication with AuthController and services
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RacePlannerApi.Data;
|
||||
using RacePlannerApi.DTOs;
|
||||
using RacePlannerApi.Models;
|
||||
using RacePlannerApi.Services;
|
||||
|
||||
namespace RacePlannerApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly RacePlannerDbContext _context;
|
||||
private readonly JwtTokenService _jwtService;
|
||||
|
||||
public AuthController(RacePlannerDbContext context, JwtTokenService jwtService)
|
||||
{
|
||||
_context = context;
|
||||
_jwtService = jwtService;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult<AuthResponse>> Register(RegisterRequest request)
|
||||
{
|
||||
// Check if email already exists
|
||||
if (await _context.Users.AnyAsync(u => u.Email == request.Email))
|
||||
{
|
||||
return Conflict(new { error = "Email already registered" });
|
||||
}
|
||||
|
||||
// Create new user
|
||||
var user = new User
|
||||
{
|
||||
Email = request.Email,
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password),
|
||||
Name = request.Name,
|
||||
Role = request.Role
|
||||
};
|
||||
|
||||
_context.Users.Add(user);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Generate token
|
||||
var token = _jwtService.GenerateToken(user);
|
||||
|
||||
return Ok(new AuthResponse
|
||||
{
|
||||
Token = token,
|
||||
User = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Name = user.Name,
|
||||
Role = user.Role.ToString()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<AuthResponse>> Login(LoginRequest request)
|
||||
{
|
||||
// Find user by email
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new { error = "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
|
||||
{
|
||||
return Unauthorized(new { error = "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Generate token
|
||||
var token = _jwtService.GenerateToken(user);
|
||||
|
||||
return Ok(new AuthResponse
|
||||
{
|
||||
Token = token,
|
||||
User = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Name = user.Name,
|
||||
Role = user.Role.ToString()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using RacePlannerApi.Models;
|
||||
|
||||
namespace RacePlannerApi.DTOs;
|
||||
|
||||
public class RegisterRequest
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(8)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public UserRole Role { get; set; } = UserRole.Participant;
|
||||
}
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AuthResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public UserDto User { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class UserDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Role { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using RacePlannerApi.Data;
|
||||
using RacePlannerApi.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -8,6 +12,29 @@ builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// Configure JWT Authentication
|
||||
var jwtKey = builder.Configuration["Jwt:Key"] ?? "your-secret-key-here-minimum-32-characters-long";
|
||||
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "RacePlannerApi";
|
||||
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "RacePlannerClient";
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtIssuer,
|
||||
ValidAudience = jwtAudience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
|
||||
};
|
||||
});
|
||||
|
||||
// Register services
|
||||
builder.Services.AddScoped<JwtTokenService>();
|
||||
|
||||
// Configure Entity Framework Core with PostgreSQL
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? "Host=localhost;Database=RacePlanner;Username=postgres;Password=postgres";
|
||||
@@ -40,6 +67,8 @@ app.UseHttpsRedirection();
|
||||
// Apply CORS
|
||||
app.UseCors("AllowFrontend");
|
||||
|
||||
// Authentication & Authorization
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using RacePlannerApi.Models;
|
||||
|
||||
namespace RacePlannerApi.Services;
|
||||
|
||||
public class JwtTokenService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public JwtTokenService(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public string GenerateToken(User user)
|
||||
{
|
||||
var securityKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(_configuration["Jwt:Key"] ?? "your-secret-key-here-minimum-32-characters-long"));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(JwtRegisteredClaimNames.Name, user.Name),
|
||||
new Claim("role", user.Role.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _configuration["Jwt:Issuer"] ?? "RacePlannerApi",
|
||||
audience: _configuration["Jwt:Audience"] ?? "RacePlannerClient",
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddHours(24),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
## 1. Project Setup
|
||||
|
||||
- [ ] 1.1 Initialize project structure with backend and frontend directories
|
||||
- [ ] 1.2 Setup .NET backend (ASP.NET Core Web API with C#)
|
||||
- [ ] 1.3 Setup Next.js frontend with TypeScript using Bun runtime
|
||||
- [ ] 1.4 Configure ESLint and Prettier for frontend
|
||||
- [ ] 1.5 Setup Entity Framework Core with PostgreSQL
|
||||
- [ ] 1.6 Create initial database schema migration (EF Core)
|
||||
- [x] 1.1 Initialize project structure with backend and frontend directories
|
||||
- [x] 1.2 Setup .NET backend (ASP.NET Core Web API with C#)
|
||||
- [x] 1.3 Setup Next.js frontend with TypeScript using Bun runtime
|
||||
- [x] 1.4 Configure ESLint and Prettier for frontend
|
||||
- [x] 1.5 Setup Entity Framework Core with PostgreSQL
|
||||
- [x] 1.6 Create initial database schema migration (EF Core)
|
||||
|
||||
## 2. Database Schema
|
||||
|
||||
- [ ] 2.1 Create users table with roles
|
||||
- [ ] 2.2 Create events table with organizer foreign key
|
||||
- [ ] 2.3 Create registrations table with event and user foreign keys
|
||||
- [ ] 2.4 Create payments table with registration foreign key
|
||||
- [ ] 2.5 Create announcements table with event foreign key
|
||||
- [ ] 2.6 Add database indexes for common queries
|
||||
- [x] 2.1 Create users table with roles
|
||||
- [x] 2.2 Create events table with organizer foreign key
|
||||
- [x] 2.3 Create registrations table with event and user foreign keys
|
||||
- [x] 2.4 Create payments table with registration foreign key
|
||||
- [x] 2.5 Create announcements table with event foreign key
|
||||
- [x] 2.6 Add database indexes for common queries
|
||||
|
||||
## 3. User Authentication (user-auth)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user