diff --git a/Streetcode/Streetcode.WebApi/Controllers/AdminController.cs b/Streetcode/Streetcode.WebApi/Controllers/AdminController.cs new file mode 100644 index 000000000..04b493b83 --- /dev/null +++ b/Streetcode/Streetcode.WebApi/Controllers/AdminController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Streetcode.WebApi.Controllers +{ + [Route("admin")] + [Authorize(Roles = "Admin")] + public class AdminController : Controller + { + // Admin Panel Access + public IActionResult AdminPanel() + { + return View(); // Admin panel logic + } + + // For Unauthorized Users + [AllowAnonymous] + public IActionResult UnauthorizedAccess() + { + return NotFound(); // Return 404 page for unauthorized users + } + } +} diff --git a/Streetcode/Streetcode.WebApi/Controllers/Authentication/AuthController.cs b/Streetcode/Streetcode.WebApi/Controllers/Authentication/AuthController.cs index 1ce70b27a..815acaf7d 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Authentication/AuthController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Authentication/AuthController.cs @@ -1,6 +1,8 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.Logging; using Streetcode.BLL.DTO.Authentication.Login; using Streetcode.BLL.DTO.Authentication.RefreshToken; using Streetcode.BLL.DTO.Authentication.Register; @@ -12,32 +14,70 @@ namespace Streetcode.WebApi.Controllers.Authentication { + public static class Roles + { + public const string Admin = "Admin"; + public const string User = "User"; + } + [ApiController] + [EnableRateLimiting("api")] // General rate limiting for all endpoints + [Authorize] // Require authentication by default + [Route("api/[controller]")] public class AuthController : BaseApiController { - [HttpPost] + private readonly ILogger _logger; + + public AuthController(ILogger logger) + { + _logger = logger; + } + + [AllowAnonymous] // Explicitly allow unauthenticated users + [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(LoginResponseDTO))] public async Task Login([FromBody] LoginRequestDTO loginDTO) { + _logger.LogInformation("Login attempt received."); return HandleResult(await Mediator.Send(new LoginQuery(loginDTO))); } - [HttpPost] + [AllowAnonymous] + [HttpPost("register")] + [EnableRateLimiting("registration")] // Stricter rate limiting for registration + [ValidateAntiForgeryToken] // CSRF protection [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RegisterResponseDTO))] public async Task Register([FromBody] RegisterRequestDTO registerDTO) { + _logger.LogInformation("New user registration attempt received."); // No PII logging + + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + return HandleResult(await Mediator.Send(new RegisterQuery(registerDTO))); } - [HttpPost] + [AllowAnonymous] + [HttpPost("refresh-token")] + [EnableRateLimiting("token-refresh")] // Stricter rate limiting for token refresh + [ValidateAntiForgeryToken] // CSRF protection [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RefreshTokenResponceDTO))] public async Task RefreshToken([FromBody] RefreshTokenRequestDTO token) { + _logger.LogInformation("Refresh token attempt."); + + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + return HandleResult(await Mediator.Send(new RefreshTokenQuery(token))); } [Authorize] - [HttpPost] + [HttpPost("logout")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task Logout() { @@ -45,6 +85,7 @@ public async Task Logout() if (string.IsNullOrEmpty(userId)) { + _logger.LogWarning("Unauthorized logout attempt."); return Unauthorized("User is not authenticated."); } @@ -52,16 +93,20 @@ public async Task Logout() if (result.IsFailed) { + _logger.LogError("Logout failed for user: {UserId}", userId); return BadRequest(result.Errors.First().Message); } + _logger.LogInformation("User {UserId} logged out successfully.", userId); return Ok("Logout successful. Refresh token invalidated."); } - [HttpPost] + [AllowAnonymous] + [HttpPost("google-login")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(LoginResponseDTO))] public async Task GoogleLogin([FromBody] string idToken) { + _logger.LogInformation("Google login attempt."); var result = await Mediator.Send(new LoginGoogleQuery(idToken)); if (result.IsSuccess) @@ -69,7 +114,18 @@ public async Task GoogleLogin([FromBody] string idToken) return Ok(result.Value); } + _logger.LogWarning("Google login failed."); return Unauthorized(new { message = result.Errors.FirstOrDefault()?.Message }); } + + // Admin-only endpoint to retrieve users + [Authorize(Roles = Roles.Admin)] + [HttpGet("users")] + public async Task GetUsers() + { + _logger.LogInformation("Admin accessing user list."); + // Implementation of user retrieval logic here + return Ok(new { message = "User list retrieved successfully." }); + } } } diff --git a/Streetcode/Streetcode.WebApi/Controllers/ProfileController.cs b/Streetcode/Streetcode.WebApi/Controllers/ProfileController.cs new file mode 100644 index 000000000..a1d1ac070 --- /dev/null +++ b/Streetcode/Streetcode.WebApi/Controllers/ProfileController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Streetcode.WebApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ProfileController : ControllerBase + { + [HttpGet] + [Authorize] + public IActionResult GetProfile() + { + var currentUser = User.Identity.Name; // Or use user ID to identify + + if (User.IsInRole("Admin")) + { + return NotFound(); // Return 404 if admin attempts to access their profile + } + + // Logic for retrieving the profile goes here + return Ok("Profile information"); + } + + [HttpPut] + [Authorize] + public IActionResult UpdateProfile([FromBody] string newProfileInfo) + { + var currentUser = User.Identity.Name; + + if (User.IsInRole("Admin")) + { + return NotFound(); // Return 404 if admin attempts to update their profile + } + + // Logic for updating the profile goes here + return Ok("Profile updated successfully"); + } + } +} diff --git a/Streetcode/Streetcode.WebApi/Program.cs b/Streetcode/Streetcode.WebApi/Program.cs index eb6a99910..99309cb2a 100644 --- a/Streetcode/Streetcode.WebApi/Program.cs +++ b/Streetcode/Streetcode.WebApi/Program.cs @@ -14,7 +14,10 @@ builder.Host.ConfigureApplication(builder); +// Localization builder.Services.AddLocalization(option => option.ResourcesPath = "Resources"); + +// Add application services builder.Services.AddApplicationServices(builder.Configuration); builder.Services.AddSwaggerServices(); builder.Services.AddCustomServices(); @@ -25,13 +28,35 @@ builder.Services.ConfigureRequestResponseMiddlewareOptions(builder); builder.Services.ConfigureRateLimitMiddleware(builder); builder.Services.ConfigureResponseCompressingMiddleware(builder); + +// Configure forwarded headers builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); + +// ✅ **Enhanced Rate Limiting Configuration** builder.Services.AddRateLimiter(options => { + options.AddFixedWindowLimiter("api", opt => + { + opt.Window = TimeSpan.FromMinutes(1); + opt.PermitLimit = 100; // General API: 100 requests per minute + }); + + options.AddFixedWindowLimiter("registration", opt => + { + opt.Window = TimeSpan.FromMinutes(10); + opt.PermitLimit = 5; // Registration: 5 attempts per 10 minutes + }); + + options.AddFixedWindowLimiter("token-refresh", opt => + { + opt.Window = TimeSpan.FromMinutes(5); + opt.PermitLimit = 10; // Token Refresh: 10 attempts per 5 minutes + }); + options.AddPolicy("EmailRateLimit", context => RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(), factory: _ => new FixedWindowRateLimiterOptions @@ -42,6 +67,7 @@ Window = TimeSpan.FromMinutes(5) })); + // Global Rate Limiting Handler options.OnRejected = async (context, token) => { context.HttpContext.Response.StatusCode = 429; @@ -49,12 +75,14 @@ }; }); +// Add HttpClient builder.Services.AddHttpClient(); var app = builder.Build(); app.UseForwardedHeaders(); +// ✅ **Enhanced Localization** var supportedCulture = new[] { new CultureInfo("en-US"), @@ -67,6 +95,8 @@ SupportedUICultures = supportedCulture, ApplyCurrentCultureToResponseHeaders = true }); + +// ✅ **Swagger only for non-production** if (app.Environment.EnvironmentName != "Production") { app.UseSwagger(); @@ -77,10 +107,14 @@ app.UseHsts(); } +// Apply migrations await app.ApplyMigrations(); +// Hangfire jobs app.AddCleanAudiosJob(); app.AddCleanImagesJob(); + +// ✅ **Security & Middleware** app.UseCors(); app.UseHttpsRedirection(); app.UseRequestResponseMiddleware(); @@ -92,9 +126,11 @@ Authorization = new[] { new HangfireDashboardAuthorizationFilter() } }); +// ✅ **Enable Rate Limiting & IP Rate Limiting** app.UseIpRateLimiting(); app.UseRateLimiter(); +// ✅ **Background Jobs** BackgroundJob.Schedule( wp => wp.ParseZipFileFromWebAsync(), TimeSpan.FromMinutes(1)); RecurringJob.AddOrUpdate( @@ -102,10 +138,14 @@ wp => wp.ParseZipFileFromWebAsync(), Cron.Monthly); +// ✅ **Ensure All Controllers Are Mapped** app.MapControllers(); + +// ✅ **Custom Response Compression Middleware** app.UseMiddleware(); app.Run(); + public partial class Program { -} \ No newline at end of file +}