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
140 changes: 140 additions & 0 deletions api/CourseRegistration.API/Controllers/CertificatesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using Microsoft.AspNetCore.Mvc;
using CourseRegistration.Application.Services;
using CourseRegistration.Application.DTOs;

namespace CourseRegistration.API.Controllers;

/// <summary>
/// API controller for certificate operations
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class CertificatesController : ControllerBase
{
private readonly ICertificateService _certificateService;
private readonly ILogger<CertificatesController> _logger;

public CertificatesController(ICertificateService certificateService, ILogger<CertificatesController> logger)
{
_certificateService = certificateService;
_logger = logger;
}

/// <summary>
/// Get certificate by ID
/// </summary>
/// <param name="id">Certificate ID</param>
/// <returns>Certificate details</returns>
[HttpGet("{id}")]
[ProducesResponseType(typeof(CertificateDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CertificateDto>> GetCertificateById(Guid id)
{
try
{
_logger.LogInformation("Getting certificate with ID: {CertificateId}", id);
var certificate = await _certificateService.GetCertificateByIdAsync(id);

if (certificate == null)
{
_logger.LogWarning("Certificate not found: {CertificateId}", id);
return NotFound(new { message = $"Certificate with ID {id} not found" });
}

return Ok(certificate);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving certificate {CertificateId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
new { message = "An error occurred while retrieving the certificate" });
}
Comment on lines +46 to +51
}

/// <summary>
/// Get certificates by student ID
/// </summary>
/// <param name="studentId">Student ID</param>
/// <returns>List of certificates for the student</returns>
[HttpGet("student/{studentId}")]
[ProducesResponseType(typeof(IEnumerable<CertificateDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CertificateDto>>> GetCertificatesByStudentId(Guid studentId)
{
try
{
_logger.LogInformation("Getting certificates for student: {StudentId}", studentId);
var certificates = await _certificateService.GetCertificatesByStudentIdAsync(studentId);
return Ok(certificates);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving certificates for student {StudentId}", studentId);
return StatusCode(StatusCodes.Status500InternalServerError,
new { message = "An error occurred while retrieving certificates" });
}
Comment on lines +69 to +74
}

/// <summary>
/// Search certificates by student name
/// </summary>
/// <param name="studentName">Student name (partial match supported)</param>
/// <returns>List of matching certificates</returns>
[HttpGet("search")]
[ProducesResponseType(typeof(IEnumerable<CertificateDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CertificateDto>>> SearchCertificates([FromQuery] string studentName)
{
try
{
if (string.IsNullOrWhiteSpace(studentName))
{
return BadRequest(new { message = "Student name is required" });
}

_logger.LogInformation("Searching certificates for student name: {StudentName}", studentName);

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI about 1 month ago

To fix the problem, sanitize the studentName parameter to remove (or otherwise escape) any newline or carriage return characters before logging it. This should be done immediately before the logging statement, only for the purpose of recording the log, so as not to affect the actual application logic or query.
The fix involves:

  1. Defining a local variable, e.g., safeStudentName, just before logging.
  2. Assigning safeStudentName the value of studentName with all newline-related characters removed (\r, \n, and optionally others).
  3. Passing safeStudentName to the logger in place of the raw studentName.

Only lines in the method containing the logged user data should be changed. No changes are needed elsewhere.


Suggested changeset 1
api/CourseRegistration.API/Controllers/CertificatesController.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/CertificatesController.cs b/api/CourseRegistration.API/Controllers/CertificatesController.cs
--- a/api/CourseRegistration.API/Controllers/CertificatesController.cs
+++ b/api/CourseRegistration.API/Controllers/CertificatesController.cs
@@ -90,7 +90,9 @@
                 return BadRequest(new { message = "Student name is required" });
             }
 
-            _logger.LogInformation("Searching certificates for student name: {StudentName}", studentName);
+            // Remove newlines from user input before logging to prevent log forging
+            var safeStudentName = studentName.Replace("\r", "").Replace("\n", "");
+            _logger.LogInformation("Searching certificates for student name: {StudentName}", safeStudentName);
             var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName);
             return Ok(certificates);
         }
EOF
@@ -90,7 +90,9 @@
return BadRequest(new { message = "Student name is required" });
}

_logger.LogInformation("Searching certificates for student name: {StudentName}", studentName);
// Remove newlines from user input before logging to prevent log forging
var safeStudentName = studentName.Replace("\r", "").Replace("\n", "");
_logger.LogInformation("Searching certificates for student name: {StudentName}", safeStudentName);
var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName);
return Ok(certificates);
}
Copilot is powered by AI and may make mistakes. Always verify output.
var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName);
return Ok(certificates);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching certificates for student name {StudentName}", studentName);

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI about 1 month ago

The best way to fix this problem is to sanitize the studentName variable before logging it. Specifically, remove any line break characters from the value, since log forging usually relies on inserting newlines. In general terms, update the logging line at line 99 (and optionally at line 93 for consistency) to log a sanitized version of studentName in place of the raw input.

Detailed steps:

  • Before logging at line 99, create a sanitized version of studentName by removing (replacing) all occurrences of \r and \n.
  • Use the sanitized value in the logging statement.
  • Do this for line 99.
  • (Optional/recommended: do the same for line 93, which also logs user input.)

No new imports are required, as string.Replace is built in.
Only lines within the SearchCertificates method in api/CourseRegistration.API/Controllers/CertificatesController.cs are changed.


Suggested changeset 1
api/CourseRegistration.API/Controllers/CertificatesController.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/CertificatesController.cs b/api/CourseRegistration.API/Controllers/CertificatesController.cs
--- a/api/CourseRegistration.API/Controllers/CertificatesController.cs
+++ b/api/CourseRegistration.API/Controllers/CertificatesController.cs
@@ -90,13 +90,16 @@
                 return BadRequest(new { message = "Student name is required" });
             }
 
-            _logger.LogInformation("Searching certificates for student name: {StudentName}", studentName);
+            // Sanitize user input before logging to prevent log forging by removing newlines
+            var sanitizedStudentName = studentName.Replace("\r", "").Replace("\n", "");
+            _logger.LogInformation("Searching certificates for student name: {StudentName}", sanitizedStudentName);
             var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName);
             return Ok(certificates);
         }
         catch (Exception ex)
         {
-            _logger.LogError(ex, "Error searching certificates for student name {StudentName}", studentName);
+            var sanitizedStudentName = studentName?.Replace("\r", "").Replace("\n", "");
+            _logger.LogError(ex, "Error searching certificates for student name {StudentName}", sanitizedStudentName);
             return StatusCode(StatusCodes.Status500InternalServerError, 
                 new { message = "An error occurred while searching certificates" });
         }
EOF
@@ -90,13 +90,16 @@
return BadRequest(new { message = "Student name is required" });
}

_logger.LogInformation("Searching certificates for student name: {StudentName}", studentName);
// Sanitize user input before logging to prevent log forging by removing newlines
var sanitizedStudentName = studentName.Replace("\r", "").Replace("\n", "");
_logger.LogInformation("Searching certificates for student name: {StudentName}", sanitizedStudentName);
var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName);
return Ok(certificates);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching certificates for student name {StudentName}", studentName);
var sanitizedStudentName = studentName?.Replace("\r", "").Replace("\n", "");
_logger.LogError(ex, "Error searching certificates for student name {StudentName}", sanitizedStudentName);
return StatusCode(StatusCodes.Status500InternalServerError,
new { message = "An error occurred while searching certificates" });
}
Copilot is powered by AI and may make mistakes. Always verify output.
return StatusCode(StatusCodes.Status500InternalServerError,
new { message = "An error occurred while searching certificates" });
}
Comment on lines +97 to +102
}

/// <summary>
/// Create a new certificate
/// </summary>
/// <param name="createCertificateDto">Certificate creation data</param>
/// <returns>Created certificate</returns>
[HttpPost]
[ProducesResponseType(typeof(CertificateDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<CertificateDto>> CreateCertificate([FromBody] CreateCertificateDto createCertificateDto)
{
try
{
if (createCertificateDto == null)
{
return BadRequest(new { message = "Certificate data is required" });
}

_logger.LogInformation("Creating certificate for student {StudentId} and course {CourseId}",
createCertificateDto.StudentId, createCertificateDto.CourseId);

var certificate = await _certificateService.CreateCertificateAsync(createCertificateDto);

return CreatedAtAction(
nameof(GetCertificateById),
new { id = certificate.CertificateId },
certificate
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating certificate");
return StatusCode(StatusCodes.Status500InternalServerError,
new { message = "An error occurred while creating the certificate" });
}
Comment on lines +133 to +138
}
}
53 changes: 51 additions & 2 deletions api/CourseRegistration.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
builder.Services.AddScoped<IStudentService, StudentService>();
builder.Services.AddScoped<ICourseService, CourseService>();
builder.Services.AddScoped<IRegistrationService, RegistrationService>();
builder.Services.AddScoped<ICertificateService, CertificateService>();

// Register authorization services
builder.Services.AddScoped<AuthorizationService>();
Expand Down Expand Up @@ -299,6 +300,54 @@ static async Task SeedDatabase(CourseRegistrationDbContext context)
context.Registrations.AddRange(registrations);
await context.SaveChangesAsync();

Log.Information("Database seeded successfully with {StudentCount} students, {CourseCount} courses, and {RegistrationCount} registrations.",
students.Length, courses.Length, registrations.Length);
// Sample certificates for completed courses
var certificates = new[]
{
new CourseRegistration.Domain.Entities.Certificate
{
StudentId = students[0].StudentId,
CourseId = courses[0].CourseId,
IssueDate = DateTime.UtcNow.AddDays(-30),
FinalGrade = CourseRegistration.Domain.Enums.Grade.A,
CertificateNumber = "CERT-2024-001",
Remarks = "Outstanding performance throughout the course",
DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8]
},
new CourseRegistration.Domain.Entities.Certificate
{
StudentId = students[1].StudentId,
CourseId = courses[2].CourseId,
IssueDate = DateTime.UtcNow.AddDays(-20),
FinalGrade = CourseRegistration.Domain.Enums.Grade.B,
CertificateNumber = "CERT-2024-002",
Remarks = "Strong practical skills demonstrated",
DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8]
},
new CourseRegistration.Domain.Entities.Certificate
{
StudentId = students[2].StudentId,
CourseId = courses[4].CourseId,
IssueDate = DateTime.UtcNow.AddDays(-10),
FinalGrade = CourseRegistration.Domain.Enums.Grade.A,
CertificateNumber = "CERT-2024-003",
Remarks = "Exceptional database design project",
DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8]
},
new CourseRegistration.Domain.Entities.Certificate
{
StudentId = students[3].StudentId,
CourseId = courses[1].CourseId,
IssueDate = DateTime.UtcNow.AddDays(-5),
FinalGrade = CourseRegistration.Domain.Enums.Grade.B,
CertificateNumber = "CERT-2024-004",
Remarks = "Good analytical and problem-solving skills",
DigitalSignature = "DS-" + Guid.NewGuid().ToString()[..8]
}
};

context.Certificates.AddRange(certificates);
await context.SaveChangesAsync();

Log.Information("Database seeded successfully with {StudentCount} students, {CourseCount} courses, {RegistrationCount} registrations, and {CertificateCount} certificates.",
students.Length, courses.Length, registrations.Length, certificates.Length);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public CourseRegistrationDbContext(DbContextOptions<CourseRegistrationDbContext>
/// </summary>
public DbSet<Registration> Registrations { get; set; } = null!;

/// <summary>
/// Certificates DbSet
/// </summary>
public DbSet<Certificate> Certificates { get; set; } = null!;

/// <summary>
/// Configures the model relationships and constraints
/// </summary>
Expand Down Expand Up @@ -108,6 +113,34 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasForeignKey(r => r.CourseId)
.OnDelete(DeleteBehavior.Cascade);
});

// Configure Certificate entity
modelBuilder.Entity<Certificate>(entity =>
{
entity.HasKey(c => c.CertificateId);
entity.Property(c => c.StudentId).IsRequired();
entity.Property(c => c.CourseId).IsRequired();
entity.Property(c => c.IssueDate).IsRequired();
entity.Property(c => c.FinalGrade).IsRequired()
.HasConversion<string>();
entity.Property(c => c.CertificateNumber).IsRequired().HasMaxLength(20);
entity.Property(c => c.Remarks).HasMaxLength(200);
entity.Property(c => c.DigitalSignature).HasMaxLength(100);

// Create unique constraint for certificate number
entity.HasIndex(c => c.CertificateNumber).IsUnique();

// Configure relationships
entity.HasOne(c => c.Student)
.WithMany()
.HasForeignKey(c => c.StudentId)
.OnDelete(DeleteBehavior.Cascade);

entity.HasOne(c => c.Course)
.WithMany()
.HasForeignKey(c => c.CourseId)
.OnDelete(DeleteBehavior.Cascade);
});
}

/// <summary>
Expand Down
57 changes: 57 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ <h1>Course Registration System</h1>
<i class="fas fa-user-check"></i>
Registrations
</a>
<a href="#" class="nav-link" onclick="showCertificates(event)">
<i class="fas fa-certificate"></i>
Certificates
</a>
</nav>
</div>
</header>
Expand Down Expand Up @@ -269,6 +273,43 @@ <h3>No registrations found</h3>
</button>
</div>
</section>

<!-- Certificates Section -->
<section id="certificates-section" class="section hidden">
<div class="section-header">
<h2>
<i class="fas fa-certificate"></i>
Certificates
</h2>
<p>Search and view digital certificates</p>
</div>

<div class="search-container">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="certificate-search-input" placeholder="Search by student name or certificate ID..." onkeyup="searchCertificates()">
</div>
<button class="btn btn-secondary" onclick="searchCertificates()">
<i class="fas fa-search"></i>
Search
</button>
</div>

<div id="certificates-container" class="certificates-grid">
<!-- Certificates will be loaded here dynamically -->
</div>

<div id="certificates-loading" class="loading hidden">
<i class="fas fa-spinner fa-spin"></i>
Searching certificates...
</div>

<div id="certificates-empty" class="empty-state hidden">
<i class="fas fa-certificate"></i>
<h3>No certificates found</h3>
<p>Try searching by student name or certificate ID</p>
</div>
</section>
</main>

<!-- Success Modal -->
Expand Down Expand Up @@ -404,6 +445,22 @@ <h3>Registration Details</h3>
</div>
</div>
</div>

<!-- Certificate Details Modal -->
<div id="certificate-details-modal" class="modal hidden">
<div class="modal-content certificate-modal">
<div class="modal-header">
<i class="fas fa-certificate"></i>
<h3>Certificate Details</h3>
<button class="modal-close" onclick="closeModal('certificate-details-modal')">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body" id="certificate-details-content">
<!-- Certificate details will be loaded here -->
</div>
</div>
</div>
</div>

<!-- Loading Overlay -->
Expand Down
Loading