Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Streetcode/Streetcode.WebApi/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,64 +14,118 @@

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<AuthController> _logger;

public AuthController(ILogger<AuthController> logger)
{
_logger = logger;
}

[AllowAnonymous] // Explicitly allow unauthenticated users
[HttpPost("login")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(LoginResponseDTO))]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Logout()
{
var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;

if (string.IsNullOrEmpty(userId))
{
_logger.LogWarning("Unauthorized logout attempt.");
return Unauthorized("User is not authenticated.");
}

var result = await Mediator.Send(new LogoutCommand(userId));

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<IActionResult> GoogleLogin([FromBody] string idToken)
{
_logger.LogInformation("Google login attempt.");
var result = await Mediator.Send(new LoginGoogleQuery(idToken));

if (result.IsSuccess)
{
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<IActionResult> GetUsers()
{
_logger.LogInformation("Admin accessing user list.");
// Implementation of user retrieval logic here
return Ok(new { message = "User list retrieved successfully." });
}
}
}
40 changes: 40 additions & 0 deletions Streetcode/Streetcode.WebApi/Controllers/ProfileController.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
42 changes: 41 additions & 1 deletion Streetcode/Streetcode.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -25,13 +28,35 @@
builder.Services.ConfigureRequestResponseMiddlewareOptions(builder);
builder.Services.ConfigureRateLimitMiddleware(builder);
builder.Services.ConfigureResponseCompressingMiddleware(builder);

// Configure forwarded headers
builder.Services.Configure<ForwardedHeadersOptions>(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
Expand All @@ -42,19 +67,22 @@
Window = TimeSpan.FromMinutes(5)
}));

// Global Rate Limiting Handler
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync("Too many requests.", cancellationToken: token);
};
});

// Add HttpClient
builder.Services.AddHttpClient();

var app = builder.Build();

app.UseForwardedHeaders();

// ✅ **Enhanced Localization**
var supportedCulture = new[]
{
new CultureInfo("en-US"),
Expand All @@ -67,6 +95,8 @@
SupportedUICultures = supportedCulture,
ApplyCurrentCultureToResponseHeaders = true
});

// ✅ **Swagger only for non-production**
if (app.Environment.EnvironmentName != "Production")
{
app.UseSwagger();
Expand All @@ -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();
Expand All @@ -92,20 +126,26 @@
Authorization = new[] { new HangfireDashboardAuthorizationFilter() }
});

// ✅ **Enable Rate Limiting & IP Rate Limiting**
app.UseIpRateLimiting();
app.UseRateLimiter();

// ✅ **Background Jobs**
BackgroundJob.Schedule<WebParsingUtils>(
wp => wp.ParseZipFileFromWebAsync(), TimeSpan.FromMinutes(1));
RecurringJob.AddOrUpdate<WebParsingUtils>(
"ParseZipFileFromWebAsync",
wp => wp.ParseZipFileFromWebAsync(),
Cron.Monthly);

// ✅ **Ensure All Controllers Are Mapped**
app.MapControllers();

// ✅ **Custom Response Compression Middleware**
app.UseMiddleware<CustomResponseCompressionMiddleware>();

app.Run();

public partial class Program
{
}
}