Skip to content
Draft
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
207 changes: 207 additions & 0 deletions api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using Microsoft.AspNetCore.Mvc;
using CourseRegistration.Application.DTOs;
using CourseRegistration.Application.Interfaces;

namespace CourseRegistration.API.Controllers;

/// <summary>
/// Controller for instructor rating operations
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class InstructorRatingsController : ControllerBase
{
private readonly IInstructorRatingService _ratingService;
private readonly ILogger<InstructorRatingsController> _logger;

/// <summary>
/// Initializes a new instance of the InstructorRatingsController
/// </summary>
public InstructorRatingsController(IInstructorRatingService ratingService, ILogger<InstructorRatingsController> logger)
{
_ratingService = ratingService ?? throw new ArgumentNullException(nameof(ratingService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

/// <summary>
/// Creates a new instructor rating
/// </summary>
/// <param name="createRatingDto">Rating creation details</param>
/// <returns>Created rating</returns>
[HttpPost]
[ProducesResponseType(typeof(ApiResponseDto<InstructorRatingDto>), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> CreateRating([FromBody] CreateInstructorRatingDto createRatingDto)
{
_logger.LogInformation("Creating new rating for course {CourseId} by student {StudentId}",
createRatingDto.CourseId, createRatingDto.StudentId);

try
{
var rating = await _ratingService.CreateRatingAsync(createRatingDto);

return CreatedAtAction(
nameof(GetRating),
new { id = rating.RatingId },
ApiResponseDto<InstructorRatingDto>.SuccessResponse(rating, "Rating created successfully"));
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Failed to create rating: {Message}", ex.Message);
return BadRequest(ApiResponseDto<InstructorRatingDto>.ErrorResponse(ex.Message));
}
}

/// <summary>
/// Gets a rating by ID
/// </summary>
/// <param name="id">Rating ID</param>
/// <returns>Rating details</returns>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(ApiResponseDto<InstructorRatingDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> GetRating(Guid id)
{
_logger.LogInformation("Getting rating with ID: {RatingId}", id);

var rating = await _ratingService.GetRatingByIdAsync(id);
if (rating == null)
{
return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found"));
}

return Ok(ApiResponseDto<InstructorRatingDto>.SuccessResponse(rating, "Rating retrieved successfully"));
}

/// <summary>
/// Gets all ratings for a specific course
/// </summary>
/// <param name="courseId">Course ID</param>
/// <returns>List of ratings</returns>
[HttpGet("course/{courseId:guid}")]
[ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByCourse(Guid courseId)
{
_logger.LogInformation("Getting ratings for course: {CourseId}", courseId);

var ratings = await _ratingService.GetRatingsByCourseIdAsync(courseId);
return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully"));
}

/// <summary>
/// Gets all ratings submitted by a specific student
/// </summary>
/// <param name="studentId">Student ID</param>
/// <returns>List of ratings</returns>
[HttpGet("student/{studentId:guid}")]
[ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByStudent(Guid studentId)
{
_logger.LogInformation("Getting ratings by student: {StudentId}", studentId);

var ratings = await _ratingService.GetRatingsByStudentIdAsync(studentId);
return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully"));
}

/// <summary>
/// Gets all ratings for a specific instructor
/// </summary>
/// <param name="instructorName">Instructor name</param>
/// <returns>List of ratings</returns>
[HttpGet("instructor/{instructorName}")]
[ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName)
{
_logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName);

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

In general, to fix log-forging issues when logging user input, normalize or encode the input before logging. For plain-text logs, remove newline and carriage return characters (and optionally other control characters). For HTML logs, HTML-encode the value. The aim is to ensure a malicious user cannot inject extra log lines or formatting.

In this case, the best fix is to sanitize instructorName just before it is passed to the logger, by removing newline (\n) and carriage return (\r) characters. This keeps all existing functionality intact (the service call still receives the original, unsanitized instructorName for correct lookups) while ensuring that the logged version cannot create forged entries. We only need to change the body of GetRatingsByInstructor in api/CourseRegistration.API/Controllers/InstructorRatingsController.cs: introduce a local variable that holds a sanitized copy for logging and use that in _logger.LogInformation(...). No new imports are required; we can rely on string.Replace which is in System and already available.

Suggested changeset 1
api/CourseRegistration.API/Controllers/InstructorRatingsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
--- a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
+++ b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
@@ -113,7 +113,12 @@
     [ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)]
     public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName)
     {
-        _logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName);
+        var safeInstructorName = instructorName?
+            .Replace("\r\n", string.Empty)
+            .Replace("\n", string.Empty)
+            .Replace("\r", string.Empty);
+
+        _logger.LogInformation("Getting ratings for instructor: {InstructorName}", safeInstructorName);
         
         var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName);
         return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully"));
EOF
@@ -113,7 +113,12 @@
[ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName)
{
_logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName);
var safeInstructorName = instructorName?
.Replace("\r\n", string.Empty)
.Replace("\n", string.Empty)
.Replace("\r", string.Empty);

_logger.LogInformation("Getting ratings for instructor: {InstructorName}", safeInstructorName);

var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName);
return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully"));
Copilot is powered by AI and may make mistakes. Always verify output.

var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName);
return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully"));
}

/// <summary>
/// Gets rating statistics for an instructor
/// </summary>
/// <param name="instructorName">Instructor name</param>
/// <returns>Rating statistics</returns>
[HttpGet("instructor/{instructorName}/stats")]
[ProducesResponseType(typeof(ApiResponseDto<InstructorRatingStatsDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<InstructorRatingStatsDto>>> GetInstructorStats(string instructorName)
{
_logger.LogInformation("Getting stats for instructor: {InstructorName}", instructorName);

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

To fix this, we should sanitize the user-provided instructorName before it is logged, ensuring that any newline or carriage-return characters (and optionally other control characters) are removed. This prevents a malicious user from injecting extra log lines or confusing content into plain text logs while preserving the semantic value of the log entry.

The most direct, low-impact fix is to introduce a local, sanitized variable inside GetInstructorStats (and, for consistency and to eliminate a similar pattern, in GetRatingsByInstructor as well) and use that variable in the _logger.LogInformation calls. For example, we can call Replace("\r", "").Replace("\n", "") on instructorName. This keeps the method signature and its behavior unchanged for the rest of the system while ensuring that what is written to the logs cannot contain line breaks. No new imports are required; we use string’s built-in methods.

Concretely:

  • In GetRatingsByInstructor, before logging, define var sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty); and use that in the _logger.LogInformation call.
  • In GetInstructorStats (line 130+), likewise define a sanitizedInstructorName variable and use it in the log call on line 132, leaving the rest of the logic intact.
Suggested changeset 1
api/CourseRegistration.API/Controllers/InstructorRatingsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
--- a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
+++ b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
@@ -113,7 +113,8 @@
     [ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)]
     public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName)
     {
-        _logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName);
+        var sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty);
+        _logger.LogInformation("Getting ratings for instructor: {InstructorName}", sanitizedInstructorName);
         
         var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName);
         return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully"));
@@ -129,7 +130,8 @@
     [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
     public async Task<ActionResult<ApiResponseDto<InstructorRatingStatsDto>>> GetInstructorStats(string instructorName)
     {
-        _logger.LogInformation("Getting stats for instructor: {InstructorName}", instructorName);
+        var sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty);
+        _logger.LogInformation("Getting stats for instructor: {InstructorName}", sanitizedInstructorName);
         
         var stats = await _ratingService.GetInstructorStatsAsync(instructorName);
         if (stats == null)
EOF
@@ -113,7 +113,8 @@
[ProducesResponseType(typeof(ApiResponseDto<IEnumerable<InstructorRatingDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponseDto<IEnumerable<InstructorRatingDto>>>> GetRatingsByInstructor(string instructorName)
{
_logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName);
var sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty);
_logger.LogInformation("Getting ratings for instructor: {InstructorName}", sanitizedInstructorName);

var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName);
return Ok(ApiResponseDto<IEnumerable<InstructorRatingDto>>.SuccessResponse(ratings, "Ratings retrieved successfully"));
@@ -129,7 +130,8 @@
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<InstructorRatingStatsDto>>> GetInstructorStats(string instructorName)
{
_logger.LogInformation("Getting stats for instructor: {InstructorName}", instructorName);
var sanitizedInstructorName = instructorName.Replace("\r", string.Empty).Replace("\n", string.Empty);
_logger.LogInformation("Getting stats for instructor: {InstructorName}", sanitizedInstructorName);

var stats = await _ratingService.GetInstructorStatsAsync(instructorName);
if (stats == null)
Copilot is powered by AI and may make mistakes. Always verify output.

var stats = await _ratingService.GetInstructorStatsAsync(instructorName);
if (stats == null)
{
return NotFound(ApiResponseDto<InstructorRatingStatsDto>.ErrorResponse("No ratings found for this instructor"));
}

return Ok(ApiResponseDto<InstructorRatingStatsDto>.SuccessResponse(stats, "Statistics retrieved successfully"));
}

/// <summary>
/// Gets a rating by student and course
/// </summary>
/// <param name="studentId">Student ID</param>
/// <param name="courseId">Course ID</param>
/// <returns>Rating details</returns>
[HttpGet("student/{studentId:guid}/course/{courseId:guid}")]
[ProducesResponseType(typeof(ApiResponseDto<InstructorRatingDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> GetRatingByStudentAndCourse(Guid studentId, Guid courseId)
{
_logger.LogInformation("Getting rating for student {StudentId} and course {CourseId}", studentId, courseId);

var rating = await _ratingService.GetRatingByStudentAndCourseAsync(studentId, courseId);
if (rating == null)
{
return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found"));
}

return Ok(ApiResponseDto<InstructorRatingDto>.SuccessResponse(rating, "Rating retrieved successfully"));
}

/// <summary>
/// Updates an existing rating
/// </summary>
/// <param name="id">Rating ID</param>
/// <param name="updateRatingDto">Rating update details</param>
/// <returns>Updated rating</returns>
[HttpPut("{id:guid}")]
[ProducesResponseType(typeof(ApiResponseDto<InstructorRatingDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto)
{
_logger.LogInformation("Updating rating with ID: {RatingId}", id);

var rating = await _ratingService.UpdateRatingAsync(id, updateRatingDto);
if (rating == null)
{
return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found"));
}

return Ok(ApiResponseDto<InstructorRatingDto>.SuccessResponse(rating, "Rating updated successfully"));
}

/// <summary>
/// Deletes a rating
/// </summary>
/// <param name="id">Rating ID</param>
/// <returns>Success status</returns>
[HttpDelete("{id:guid}")]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id)

Check failure

Code scanning / CodeQL

Missing function level access control High

This action is missing an authorization check.

Copilot Autofix

AI 2 days ago

In general, sensitive controller actions should be protected by explicit authorization checks. For ASP.NET Core Web API, this is most commonly done using the [Authorize] attribute (optionally with roles/policies) so that only authenticated and authorized users can call these endpoints. If finer-grained control is required, additional checks can be done in the action method body (e.g., verifying that the current user is the owner of the resource or has a specific role).

The single best fix here, without changing existing functionality of the service layer, is to decorate the DeleteRating action (and, for consistency, the similarly sensitive UpdateRating action) with an [Authorize] attribute so that only authenticated users can update or delete ratings. This introduces a clear, declarative access control requirement on these methods while preserving their existing logic and return types. To implement this, we need to import Microsoft.AspNetCore.Authorization at the top of the file and add [Authorize] above the DeleteRating method. Given the similar risk profile, adding [Authorize] above UpdateRating in the same file is also recommended and consistent with the reported issue type.

Concretely in api/CourseRegistration.API/Controllers/InstructorRatingsController.cs:

  • Add using Microsoft.AspNetCore.Authorization; alongside the existing using statements.
  • Add [Authorize] above the UpdateRating method.
  • Add [Authorize] above the DeleteRating method.

No other code changes or new methods are required.

Suggested changeset 1
api/CourseRegistration.API/Controllers/InstructorRatingsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
--- a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
+++ b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
@@ -1,6 +1,7 @@
 using Microsoft.AspNetCore.Mvc;
 using CourseRegistration.Application.DTOs;
 using CourseRegistration.Application.Interfaces;
+using Microsoft.AspNetCore.Authorization;
 
 namespace CourseRegistration.API.Controllers;
 
@@ -169,6 +170,7 @@
     /// <param name="updateRatingDto">Rating update details</param>
     /// <returns>Updated rating</returns>
     [HttpPut("{id:guid}")]
+    [Authorize]
     [ProducesResponseType(typeof(ApiResponseDto<InstructorRatingDto>), StatusCodes.Status200OK)]
     [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
     public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto)
@@ -190,6 +192,7 @@
     /// <param name="id">Rating ID</param>
     /// <returns>Success status</returns>
     [HttpDelete("{id:guid}")]
+    [Authorize]
     [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status200OK)]
     [ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
     public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id)
EOF
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using CourseRegistration.Application.DTOs;
using CourseRegistration.Application.Interfaces;
using Microsoft.AspNetCore.Authorization;

namespace CourseRegistration.API.Controllers;

@@ -169,6 +170,7 @@
/// <param name="updateRatingDto">Rating update details</param>
/// <returns>Updated rating</returns>
[HttpPut("{id:guid}")]
[Authorize]
[ProducesResponseType(typeof(ApiResponseDto<InstructorRatingDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto)
@@ -190,6 +192,7 @@
/// <param name="id">Rating ID</param>
/// <returns>Success status</returns>
[HttpDelete("{id:guid}")]
[Authorize]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponseDto<object>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id)
Copilot is powered by AI and may make mistakes. Always verify output.

Check failure

Code scanning / CodeQL

Insecure Direct Object Reference High

This method may be missing authorization checks for which users can access the resource of the provided ID.

Copilot Autofix

AI 2 days ago

In general, to fix IDOR issues you must ensure that operations on an object identified by a client‑supplied ID are only allowed if the current user is authorized to access that specific object. In ASP.NET Core this is typically done by (a) retrieving the resource, (b) running an authorization check against the current User (e.g., via IAuthorizationService or a policy), and only then (c) performing the update/delete if authorized, otherwise returning Forbid() or Unauthorized().

For this controller, the least disruptive fix that keeps existing functionality is:

  • Inject IAuthorizationService into InstructorRatingsController.
  • Before calling the service methods that update or delete a rating (UpdateRating and DeleteRating), fetch the rating by ID from the service.
  • If the rating does not exist, keep returning 404 as today.
  • If it exists, call _authorizationService.AuthorizeAsync(User, rating, "InstructorRatingOwnerPolicy") (or similar) to ensure the current user is allowed to act on that rating.
    • On success, proceed with update/delete as currently.
    • On failure, return Forbid() wrapped in the same ApiResponseDto<object>/ApiResponseDto<InstructorRatingDto> style used elsewhere, to avoid changing the API contract.

Because we only see part of the file, the code changes must stay inside the shown controller. Concretely:

  • Add a using Microsoft.AspNetCore.Authorization; import at the top so we can use IAuthorizationService.
  • Add a private readonly field _authorizationService and update the controller constructor (in the shown region) to accept and assign it.
  • In UpdateRating, first call something like var existingRating = await _ratingService.GetRatingByIdAsync(id); (assuming such a method exists; if not, use whatever retrieval method is available in the shown snippet) and:
    • If existingRating == null, return 404 as today.
    • Otherwise, call _authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy").
    • If not authorized, return Forbid(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Not authorized to update this rating")).
    • Only then call _ratingService.UpdateRatingAsync(id, updateRatingDto) and return success.
  • In DeleteRating, do the same pattern: retrieve rating, check authorization, then call _ratingService.DeleteRatingAsync(id).

Since we must not assume new service methods that aren’t shown, you should adapt the retrieval to whatever method exists in the real file (e.g., GetRatingByIdAsync). In the replacement blocks below I’ll illustrate using a plausible GetRatingByIdAsync that would be present in the same service, but you may need to align the exact method name/signature with the rest of your code.

Suggested changeset 1
api/CourseRegistration.API/Controllers/InstructorRatingsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
--- a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
+++ b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs
@@ -1,6 +1,7 @@
 using Microsoft.AspNetCore.Mvc;
 using CourseRegistration.Application.DTOs;
 using CourseRegistration.Application.Interfaces;
+using Microsoft.AspNetCore.Authorization;
 
 namespace CourseRegistration.API.Controllers;
 
@@ -13,6 +14,7 @@
 public class InstructorRatingsController : ControllerBase
 {
     private readonly IInstructorRatingService _ratingService;
+    private readonly IAuthorizationService _authorizationService;
     private readonly ILogger<InstructorRatingsController> _logger;
 
     /// <summary>
@@ -174,10 +176,24 @@
     public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto)
     {
         _logger.LogInformation("Updating rating with ID: {RatingId}", id);
+
+        // Retrieve existing rating to validate authorization and existence
+        var existingRating = await _ratingService.GetRatingByIdAsync(id);
+        if (existingRating == null)
+        {
+            return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found"));
+        }
+
+        var authorizationResult = await _authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy");
+        if (!authorizationResult.Succeeded)
+        {
+            return Forbid(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Not authorized to update this rating"));
+        }
         
         var rating = await _ratingService.UpdateRatingAsync(id, updateRatingDto);
         if (rating == null)
         {
+            // In case the rating was deleted or became unavailable during update
             return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found"));
         }
 
@@ -195,6 +207,19 @@
     public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id)
     {
         _logger.LogInformation("Deleting rating with ID: {RatingId}", id);
+
+        // Retrieve existing rating to validate authorization and existence
+        var existingRating = await _ratingService.GetRatingByIdAsync(id);
+        if (existingRating == null)
+        {
+            return NotFound(ApiResponseDto<object>.ErrorResponse("Rating not found"));
+        }
+
+        var authorizationResult = await _authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy");
+        if (!authorizationResult.Succeeded)
+        {
+            return Forbid(ApiResponseDto<object>.ErrorResponse("Not authorized to delete this rating"));
+        }
         
         var success = await _ratingService.DeleteRatingAsync(id);
         if (!success)
EOF
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using CourseRegistration.Application.DTOs;
using CourseRegistration.Application.Interfaces;
using Microsoft.AspNetCore.Authorization;

namespace CourseRegistration.API.Controllers;

@@ -13,6 +14,7 @@
public class InstructorRatingsController : ControllerBase
{
private readonly IInstructorRatingService _ratingService;
private readonly IAuthorizationService _authorizationService;
private readonly ILogger<InstructorRatingsController> _logger;

/// <summary>
@@ -174,10 +176,24 @@
public async Task<ActionResult<ApiResponseDto<InstructorRatingDto>>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto)
{
_logger.LogInformation("Updating rating with ID: {RatingId}", id);

// Retrieve existing rating to validate authorization and existence
var existingRating = await _ratingService.GetRatingByIdAsync(id);
if (existingRating == null)
{
return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found"));
}

var authorizationResult = await _authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy");
if (!authorizationResult.Succeeded)
{
return Forbid(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Not authorized to update this rating"));
}

var rating = await _ratingService.UpdateRatingAsync(id, updateRatingDto);
if (rating == null)
{
// In case the rating was deleted or became unavailable during update
return NotFound(ApiResponseDto<InstructorRatingDto>.ErrorResponse("Rating not found"));
}

@@ -195,6 +207,19 @@
public async Task<ActionResult<ApiResponseDto<object>>> DeleteRating(Guid id)
{
_logger.LogInformation("Deleting rating with ID: {RatingId}", id);

// Retrieve existing rating to validate authorization and existence
var existingRating = await _ratingService.GetRatingByIdAsync(id);
if (existingRating == null)
{
return NotFound(ApiResponseDto<object>.ErrorResponse("Rating not found"));
}

var authorizationResult = await _authorizationService.AuthorizeAsync(User, existingRating, "InstructorRatingOwnerPolicy");
if (!authorizationResult.Succeeded)
{
return Forbid(ApiResponseDto<object>.ErrorResponse("Not authorized to delete this rating"));
}

var success = await _ratingService.DeleteRatingAsync(id);
if (!success)
Copilot is powered by AI and may make mistakes. Always verify output.
{
_logger.LogInformation("Deleting rating with ID: {RatingId}", id);

var success = await _ratingService.DeleteRatingAsync(id);
if (!success)
{
return NotFound(ApiResponseDto<object>.ErrorResponse("Rating not found"));
}

return Ok(ApiResponseDto<object>.SuccessResponse(null, "Rating deleted successfully"));
}
}
4 changes: 4 additions & 0 deletions api/CourseRegistration.API/CourseRegistration.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
Expand Down
2 changes: 2 additions & 0 deletions api/CourseRegistration.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@
builder.Services.AddScoped<IStudentRepository, StudentRepository>();
builder.Services.AddScoped<ICourseRepository, CourseRepository>();
builder.Services.AddScoped<IRegistrationRepository, RegistrationRepository>();
builder.Services.AddScoped<IInstructorRatingRepository, InstructorRatingRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

// Register services
builder.Services.AddScoped<IStudentService, StudentService>();
builder.Services.AddScoped<ICourseService, CourseService>();
builder.Services.AddScoped<IRegistrationService, RegistrationService>();
builder.Services.AddScoped<IInstructorRatingService, InstructorRatingService>();

// Register authorization services
builder.Services.AddScoped<AuthorizationService>();
Expand Down
160 changes: 160 additions & 0 deletions api/CourseRegistration.Application/DTOs/InstructorRatingDtos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.ComponentModel.DataAnnotations;

namespace CourseRegistration.Application.DTOs;

/// <summary>
/// DTO for creating a new instructor rating
/// </summary>
public class CreateInstructorRatingDto
{
/// <summary>
/// Course ID for which the instructor is being rated
/// </summary>
[Required]
public Guid CourseId { get; set; }

/// <summary>
/// Student ID who is submitting the rating
/// </summary>
[Required]
public Guid StudentId { get; set; }

/// <summary>
/// Rating value (1-5 scale)
/// </summary>
[Required]
[Range(1, 5, ErrorMessage = "Rating must be between 1 and 5")]
public int Rating { get; set; }

/// <summary>
/// Optional comment/review text
/// </summary>
[MaxLength(1000, ErrorMessage = "Comment cannot exceed 1000 characters")]
public string? Comment { get; set; }
}

/// <summary>
/// DTO for updating an existing instructor rating
/// </summary>
public class UpdateInstructorRatingDto
{
/// <summary>
/// Rating value (1-5 scale)
/// </summary>
[Required]
[Range(1, 5, ErrorMessage = "Rating must be between 1 and 5")]
public int Rating { get; set; }

/// <summary>
/// Optional comment/review text
/// </summary>
[MaxLength(1000, ErrorMessage = "Comment cannot exceed 1000 characters")]
public string? Comment { get; set; }
}

/// <summary>
/// DTO for returning instructor rating details
/// </summary>
public class InstructorRatingDto
{
/// <summary>
/// Unique identifier for the rating
/// </summary>
public Guid RatingId { get; set; }

/// <summary>
/// Course ID
/// </summary>
public Guid CourseId { get; set; }

/// <summary>
/// Course name
/// </summary>
public string CourseName { get; set; } = string.Empty;

/// <summary>
/// Instructor name
/// </summary>
public string InstructorName { get; set; } = string.Empty;

/// <summary>
/// Student ID who submitted the rating
/// </summary>
public Guid StudentId { get; set; }

/// <summary>
/// Student name
/// </summary>
public string StudentName { get; set; } = string.Empty;

/// <summary>
/// Rating value (1-5 scale)
/// </summary>
public int Rating { get; set; }

/// <summary>
/// Optional comment/review text
/// </summary>
public string? Comment { get; set; }

/// <summary>
/// Date when the rating was created
/// </summary>
public DateTime CreatedAt { get; set; }

/// <summary>
/// Date when the rating was last updated
/// </summary>
public DateTime UpdatedAt { get; set; }
}

/// <summary>
/// DTO for instructor rating statistics
/// </summary>
public class InstructorRatingStatsDto
{
/// <summary>
/// Instructor name
/// </summary>
public string InstructorName { get; set; } = string.Empty;

/// <summary>
/// Average rating
/// </summary>
public double AverageRating { get; set; }

/// <summary>
/// Total number of ratings
/// </summary>
public int TotalRatings { get; set; }

/// <summary>
/// Number of 5-star ratings
/// </summary>
public int FiveStarCount { get; set; }

/// <summary>
/// Number of 4-star ratings
/// </summary>
public int FourStarCount { get; set; }

/// <summary>
/// Number of 3-star ratings
/// </summary>
public int ThreeStarCount { get; set; }

/// <summary>
/// Number of 2-star ratings
/// </summary>
public int TwoStarCount { get; set; }

/// <summary>
/// Number of 1-star ratings
/// </summary>
public int OneStarCount { get; set; }

/// <summary>
/// List of courses taught by this instructor
/// </summary>
public List<Guid> CourseIds { get; set; } = new();
}
Loading
Loading