feature/new-raceplanner-app #1
@@ -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.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using RacePlannerApi.Data;
|
using RacePlannerApi.Data;
|
||||||
|
using RacePlannerApi.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -8,6 +12,29 @@ builder.Services.AddControllers();
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddOpenApi();
|
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
|
// Configure Entity Framework Core with PostgreSQL
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||||
?? "Host=localhost;Database=RacePlanner;Username=postgres;Password=postgres";
|
?? "Host=localhost;Database=RacePlanner;Username=postgres;Password=postgres";
|
||||||
@@ -40,6 +67,8 @@ app.UseHttpsRedirection();
|
|||||||
// Apply CORS
|
// Apply CORS
|
||||||
app.UseCors("AllowFrontend");
|
app.UseCors("AllowFrontend");
|
||||||
|
|
||||||
|
// Authentication & Authorization
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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.AspNetCore.OpenApi" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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. Project Setup
|
||||||
|
|
||||||
- [ ] 1.1 Initialize project structure with backend and frontend directories
|
- [x] 1.1 Initialize project structure with backend and frontend directories
|
||||||
- [ ] 1.2 Setup .NET backend (ASP.NET Core Web API with C#)
|
- [x] 1.2 Setup .NET backend (ASP.NET Core Web API with C#)
|
||||||
- [ ] 1.3 Setup Next.js frontend with TypeScript using Bun runtime
|
- [x] 1.3 Setup Next.js frontend with TypeScript using Bun runtime
|
||||||
- [ ] 1.4 Configure ESLint and Prettier for frontend
|
- [x] 1.4 Configure ESLint and Prettier for frontend
|
||||||
- [ ] 1.5 Setup Entity Framework Core with PostgreSQL
|
- [x] 1.5 Setup Entity Framework Core with PostgreSQL
|
||||||
- [ ] 1.6 Create initial database schema migration (EF Core)
|
- [x] 1.6 Create initial database schema migration (EF Core)
|
||||||
|
|
||||||
## 2. Database Schema
|
## 2. Database Schema
|
||||||
|
|
||||||
- [ ] 2.1 Create users table with roles
|
- [x] 2.1 Create users table with roles
|
||||||
- [ ] 2.2 Create events table with organizer foreign key
|
- [x] 2.2 Create events table with organizer foreign key
|
||||||
- [ ] 2.3 Create registrations table with event and user foreign keys
|
- [x] 2.3 Create registrations table with event and user foreign keys
|
||||||
- [ ] 2.4 Create payments table with registration foreign key
|
- [x] 2.4 Create payments table with registration foreign key
|
||||||
- [ ] 2.5 Create announcements table with event foreign key
|
- [x] 2.5 Create announcements table with event foreign key
|
||||||
- [ ] 2.6 Add database indexes for common queries
|
- [x] 2.6 Add database indexes for common queries
|
||||||
|
|
||||||
## 3. User Authentication (user-auth)
|
## 3. User Authentication (user-auth)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user