Skip to content

Feat/fix course authen#81

Merged
KietPham-VN merged 17 commits into
mainfrom
feat/fix-course-authen
May 11, 2026
Merged

Feat/fix course authen#81
KietPham-VN merged 17 commits into
mainfrom
feat/fix-course-authen

Conversation

@EricN2907

@EricN2907 EricN2907 commented May 10, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added mentor assignment and management for courses
    • Added answer checking for quiz questions
    • Added random question retrieval functionality
    • New mentor search, filtering, and listing endpoints
    • Added "Get Me" endpoint for retrieving current user profile
  • Bug Fixes

    • Enhanced null handling in judge result processing
  • Changes

    • Course identifiers now use Guid format
    • System migrated from Teacher to Mentor role
    • Updated user response types and DTOs
    • Improved error handling in profile updates
    • Removed teacher-related endpoints and features

EricN2907 and others added 15 commits May 5, 2026 21:54
- Updated service response handling in CompilerEndpoints, CourseEndpoints, EnrollmentEndpoints, ExamEndpoints, ExamParticipationEndpoints, ExamRoomEndpoints, HealthApi, LogBridgeEndpoints, MentorEndpoints, QuizletEndpoints, StudentSubmissionEndpoints, StudentTestCaseEndpoints, TeacherEndpoints, TestCaseEndpoints, and TestRoomEndpoints to enhance clarity and maintainability.
- Removed unnecessary using directives and organized namespaces across various files.
- Simplified response status code handling in EndpointExtensions and OpenApiConventionExtensions.
- Introduced global usings for common namespaces in Presentation, Domain, and Persistence layers to reduce boilerplate code.
@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@EricN2907 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 36 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 48af9ae3-c2be-4130-bb0d-2107aca6d548

📥 Commits

Reviewing files that changed from the base of the PR and between 1b0c1b2 and bdb14ec.

📒 Files selected for processing (6)
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs
  • backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs
  • backend/Src/PIED_LMS.Presentation/APIs/AuthenticationEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs
📝 Walkthrough

Walkthrough

This PR executes a comprehensive system-wide migration replacing the Teacher role with Mentor, converting course identifiers from int to Guid, and renaming UserResponse to UserDto. The changes span domain entities, contract types, application handlers, presentation endpoints, and infrastructure configuration, including systematic removal of all teacher-related queries and commands, and consistent authorization narrowing across exam, test, and quizlet management endpoints.

Changes

Teacher-to-Mentor Role Migration & Course GUID Transition

Layer / File(s) Summary
Domain Constants, Entities & Abstractions
backend/Src/PIED_LMS.Domain/Constants/RoleConstants.cs, backend/Src/PIED_LMS.Domain/Entities/Course.cs, backend/Src/PIED_LMS.Domain/Entities/Enrollment.cs, backend/Src/PIED_LMS.Domain/Abstractions/ICourseLockingService.cs
RoleConstants.Teacher constant removed. Course.Id changed from int to Guid. Course.Teachers navigation replaced with Course.Mentors. Enrollment.CourseId changed from int to Guid. ICourseLockingService.GetCourseForUpdateAsync signature updated to accept Guid courseId.
Identity & User DTO Contracts
backend/Src/PIED_LMS.Contract/Services/Identity/Response.cs, backend/Src/PIED_LMS.Contract/Services/Identity/Query.cs, backend/Src/PIED_LMS.Contract/Services/Identity/Validators/UpdateProfileValidator.cs
UserResponse renamed to UserDto. New GetMeQuery(Guid UserId) added for current user profile retrieval. GetUserByIdQuery, GetAllUsersQuery, GetAllStudentsQuery updated to return ServiceResponse<UserDto> instead of UserResponse. System.Linq namespace import added to validator.
Mentor Query & Response Contracts
backend/Src/PIED_LMS.Contract/Services/Mentor/Query.cs, backend/Src/PIED_LMS.Contract/Services/Mentor/Response.cs
New mentor query types: GetMentorsQuery (paginated with search/filter), GetMentorByIdQuery (by-id lookup), GetAllMentorsSimpleQuery (simple list). New response DTOs: MentorDto (full fields + IsActive), MentorSimpleDto (id, full name, profile picture URL).
Course Query, Command & Response Contracts
backend/Src/PIED_LMS.Contract/Services/Course/Query.cs, backend/Src/PIED_LMS.Contract/Services/Course/Command.cs, backend/Src/PIED_LMS.Contract/Services/Course/Request.cs, backend/Src/PIED_LMS.Contract/Services/Course/Response.cs
Course ID parameters changed from int to Guid in queries and commands. New AssignMentorsCommand (Guid CourseId, List MentorIds with validation). New request DTOs: CreateCourseRequest, UpdateCourseRequest, AssignMentorsRequest. CourseDto uses Guid Id and List<CourseMentorDto> Mentors instead of CourseTeacherDto. New CourseMentorDto record; PrerequisiteDto and StudentAvailableCourseDto use Guid Id.
Enrollment, Exam & Question Contracts
backend/Src/PIED_LMS.Contract/Services/Enrollment/Command.cs, backend/Src/PIED_LMS.Contract/Services/Enrollment/Request.cs, backend/Src/PIED_LMS.Contract/Services/Enrollment/Response.cs, backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs, backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionQuery.cs, backend/Src/PIED_LMS.Contract/Services/QuestionQuiz/Request.cs, backend/Src/PIED_LMS.Contract/Services/ExamParticipation/Query.cs, backend/Src/PIED_LMS.Contract/Services/ExamParticipation/Response.cs
EnrollCourseCommand.CourseId changed to Guid. New enrollment request types: GetAvailableCoursesRequest, GetEnrollmentsRequest. EnrollmentResponse.CourseId changed to Guid. New question DTOs: RandomQuestionResponse, CheckAnswerRequest, CheckAnswerResponse. New query types: GetRandomQuestionQuery, CheckQuestionAnswerCommand. New quizlet requests: CreateQuestionQuizRequest, UpdateQuestionQuizRequest. Comment updates reference Admin/Mentor instead of Admin/Mentor/Teacher.
Teacher Contract Type Removals
backend/Src/PIED_LMS.Contract/Services/Teacher/Query.cs, backend/Src/PIED_LMS.Contract/Services/Teacher/Response.cs
All teacher contract types removed: GetTeachersQuery, GetTeacherByIdQuery, GetAllTeachersSimpleQuery, TeacherDto, TeacherSimpleDto.
User & Profile Query Handlers
backend/Src/PIED_LMS.Application/UserCases/Queries/GetMeHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/GetUserByIdHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllUsersHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs
New GetMeHandler retrieves current user profile with role and profile picture URL resolution. GetUserByIdHandler returns ServiceResponse<UserDto> with expanded error handling. GetAllUsersHandler and GetAllStudentsHandler return List<UserDto> with profile picture URL resolution via IFileStorageService.
Mentor Query & Assignment Handlers
backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorsHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorByIdHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetAllMentorsSimpleHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs
New GetMentorsHandler implements paginated mentor retrieval with optional search and active filtering. New GetMentorByIdHandler retrieves mentor by ID with role validation. New GetAllMentorsSimpleHandler returns active mentors as simple DTO list. New AssignMentorsHandler assigns mentors to courses, validates roles, commits changes, and queues background email notifications with retry logic.
Course Handlers (Mentor Integration)
backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCoursesHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetStudentAvailableCoursesHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs, backend/Src/PIED_LMS.Application/Options/CourseManagementSettings.cs
Handlers now load/map Course.Mentors instead of Course.Teachers. Course ID parameters changed to Guid. CreateCourseHandler returns ServiceResponse<Guid> instead of ServiceResponse<int>. CourseManagementSettings.GetCourseUrl signature updated to accept Guid courseId.
Question & Quiz Handlers
backend/Src/PIED_LMS.Application/UserCases/Commands/Question/CheckQuestionAnswerHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Question/GetRandomQuestionHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Quiz/UpdateQuestionQuizHandler.cs
New GetRandomQuestionHandler retrieves single random non-hidden question from published quizlet. New CheckQuestionAnswerHandler validates user-selected answers via set equality check. UpdateQuestionQuizHandler uses fully qualified Domain.Entities.Question type for instantiation.
Teacher Handler Removals
backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetAllTeachersSimpleHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeacherByIdHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeachersHandler.cs
Entire teacher handler implementations removed, including GetAllTeachersSimpleHandler, GetTeacherByIdHandler, GetTeachersHandler, and AssignTeachersHandler with their helpers.
Utility & Supporting Handlers
backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs, backend/Src/PIED_LMS.Application/UserCases/Commands/Room/CreateRoomHandler.cs
JudgeFromFileCommandHandler adds null-check on compiler result data. UpdateProfileHandler uses explicit success/failure branch. CreateTestRoomHandler renames teacherId to mentorId.
Persistence Configuration & Locking
backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs, backend/Src/PIED_LMS.Persistence/Services/CourseLockingService.cs
CourseConfiguration switches many-to-many mapping from course_teachers to course_mentors. CourseLockingService.GetCourseForUpdateAsync signature updated to accept Guid courseId.
Authentication Endpoints
backend/Src/PIED_LMS.Presentation/APIs/AuthenticationEndpoints.cs
New /api/auth/me GET endpoint for current user profile. Updated /api/auth/users/{id}, /api/auth/users, /api/auth/students to use UserDto responses. /api/auth/students restricted to Administrator and Mentor roles. All handlers refactored to use result.ToActionResult(context).
Course Management Endpoints
backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs
Course IDs changed from int to Guid in route parameters. /api/courses/{id}/teachers removed; /api/courses/{id}/mentors added. CreateCourse and UpdateCourse refactored to use [AsParameters] request DTOs. Handler signatures updated to Guid id. AssignTeachersRequest DTO removed.
Mentor Endpoints
backend/Src/PIED_LMS.Presentation/APIs/MentorEndpoints.cs
New Carter module with admin-only routes: /api/mentors (paginated list), /api/mentors/{id} (by-id), /api/mentors/list/all (simple list). GetMentorsRequest DTO with range-validated pagination.
Enrollment & Question Endpoints
backend/Src/PIED_LMS.Presentation/APIs/EnrollmentEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs
EnrollmentEndpoints.EnrollCourse courseId changed to Guid. GetAvailableCourses and GetEnrollments refactored to use [AsParameters] request DTOs. New QuestionEndpoints module with GET /api/questions/random and POST /api/questions/check routes.
Exam & Test Authorization Updates
backend/Src/PIED_LMS.Presentation/APIs/ExamEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/ExamParticipationEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/TestCaseEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/TestRoomEndpoints.cs
Teacher role removed from authorization across exam, exam participation, exam room, test case, and test room endpoints. All mentor-scoped endpoints now require only Admin and Mentor roles. Descriptions updated to reference mentors.
Quizlet Endpoints
backend/Src/PIED_LMS.Presentation/APIs/QuizletEndpoints.cs
Teacher role removed from quizlet endpoint authorization. CreateQuizlet refactored to accept [AsParameters] CreateQuestionQuizRequest DTO. UpdateQuestionQuizRequest record declaration removed.
Teacher Endpoints & Comments
backend/Src/PIED_LMS.Presentation/APIs/TeacherEndpoints.cs, backend/Src/PIED_LMS.Presentation/APIs/StudentSubmissionEndpoints.cs
Entire TeacherEndpoints Carter module and GetTeachersRequest DTO removed. Student submission endpoint comment updated to reference Mentors instead of Teachers.
Database Initialization
backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs
Roles seeded reduced to [Administrator, Student, Mentor]. teacherPassword configuration removed; teacher seeding logic removed. mentorPassword validation added with dev/prod logging. Mentor user creation and role assignment updated with improved error handling.
Email Templates
backend/Src/PIED_LMS.Infrastructure/Email/SmtpEmailService.cs
Course assignment email template updated to use "mentor" language instead of "teacher".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PiedTeam/pied-lms#75: Prior PR introduced teacher-based course assignment handler with int IDs; this PR converts to mentor-based implementation with Guid IDs.
  • PiedTeam/pied-lms#76: Prior PR added ICourseLockingService with int courseId; this PR updates that service to use Guid courseId throughout.
  • PiedTeam/pied-lms#19: Related system-wide role/entity changes; both PRs modify mentor vs. teacher definitions and DbInitializer seeding.

Suggested reviewers

  • KietPham-VN

🎭 From ones to Guids, from Teachers to Mentors wise,
The LMS evolves with role-casting reprise!
Endpoints stand firm in Admin's demand,
While shadows of Teachers fade 'cross the land. 🚀

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fix-course-authen

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 37

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs (1)

102-110: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix malformed return block causing build failure.

The return statement is already terminated, but Lines 109-110 append extra arguments. This is exactly why CI reports CS1002 ';' expected around Line 109.

Proposed fix
         return new ServiceResponse<JudgeResult>(
             false,
             serviceResult.ErrorMessage ?? "Server is busy.",
             null,
             null,
             false,
             serviceResult.ErrorCode);
-                false,
-                serviceResult.ErrorCode);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs`
around lines 102 - 110, The return statement in JudgeFromFileCommandHandler
(inside the method handling the judge-from-file flow) is malformed: remove the
duplicated trailing tokens "false, serviceResult.ErrorCode);" that follow the
already-terminated return new ServiceResponse<JudgeResult>(...); call so the
return constructs a single valid ServiceResponse<JudgeResult> and the method
compiles; locate the return that creates ServiceResponse<JudgeResult> and delete
the extra duplicated arguments/text after the semicolon.
backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs (2)

29-35: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Close the slug race window with DB-enforced uniqueness handling.

Line 29 pre-checks slug uniqueness, but this is non-atomic with insert/commit. Under concurrency, duplicate slugs can still pass pre-check and fail later with a generic DB error (Line 94), which is the wrong functional response.

Use a unique constraint on Course.Slug and translate unique-violation DbUpdateException into the same slug-conflict response.

Suggested direction
- catch (DbUpdateException ex)
+ catch (DbUpdateException ex) when (IsSlugUniqueViolation(ex))
+ {
+     logger.LogWarning(ex, "Course creation failed: duplicate slug");
+     return new ServiceResponse<Guid>(false, "Slug already exists. Please provide a unique slug.");
+ }
+ catch (DbUpdateException ex)
  {
      logger.LogError(ex, "Database error occurred while creating course with title '{Title}'", request.Title);
      return new ServiceResponse<Guid>(false, "A database error occurred while creating the course.");
  }
// Keep provider-specific detection in one place.
private static bool IsSlugUniqueViolation(DbUpdateException ex) { /* ... */ }

Also applies to: 91-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs`
around lines 29 - 35, Pre-checking slug uniqueness with
GenerateAndValidateSlugAsync in CreateCourseHandler is racy; add a DB-level
unique constraint on Course.Slug and catch DbUpdateException on save, detecting
unique-violation (implement a helper like
IsSlugUniqueViolation(DbUpdateException ex)) and translate it into the same
slug-conflict ServiceResponse<Guid> with the existing warning log (use
logger.LogWarning with the same message) instead of letting the generic DB error
bubble up; keep the existing pre-check but ensure the catch around the
repository/DbContext save path returns the slug-conflict response when
IsSlugUniqueViolation returns true.

42-47: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Prevent orphaned thumbnails when persistence fails after upload.

Thumbnail is uploaded before DB commit. If AddAsync/CommitAsync throws, the uploaded object is never cleaned up, causing storage leaks and stale references.

Add compensating delete in failure paths after upload succeeds.

Suggested direction
 string? thumbnailPath = null;
+var thumbnailUploaded = false;

 if (request.ThumbnailFile is not null)
 {
     thumbnailPath = await fileStorageService.SaveFileAsync(...);
+    thumbnailUploaded = true;
 }

 ...
 await repository.AddAsync(course, cancellationToken);
 await unitOfWork.CommitAsync(cancellationToken);

 ...
 catch (DbUpdateException ex)
 {
+    if (thumbnailUploaded && !string.IsNullOrWhiteSpace(thumbnailPath))
+    {
+        // Adapt to your actual delete API
+        await fileStorageService.DeleteFileAsync(thumbnailPath, cancellationToken);
+    }
     logger.LogError(ex, "Database error occurred while creating course with title '{Title}'", request.Title);
     return new ServiceResponse<Guid>(false, "A database error occurred while creating the course.");
 }

Also applies to: 91-106

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs`
around lines 42 - 47, The handler uploads the thumbnail via
fileStorageService.SaveFileAsync into thumbnailPath before persisting the
Course, which can leave orphaned files if AddAsync/CommitAsync fails; update
CreateCourseHandler (where thumbnailPath is set) to wrap the persistence calls
(CourseRepository.AddAsync and UnitOfWork.CommitAsync) in a try/catch and, on
any exception, call fileStorageService.DeleteFileAsync(thumbnailPath,
cancellationToken) (or the appropriate delete method) to remove the uploaded
file, then rethrow the exception; apply the same compensating-delete pattern to
the other upload site around lines 91-106 where a thumbnail/file is uploaded
before DB commit.
backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs (2)

108-115: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Duplicate throw produces unreachable code.

Lines 113–114 are a verbatim copy of the throw at lines 111–112. The second statement is unreachable and will trigger CS0162. Delete the duplicate.

🛠️ Proposed fix
                         logger.LogError("Failed to create admin user {UserName} with role {Role}. Errors: {Errors}",
                             adminUser.UserName, RoleConstants.Administrator, errors);
                         throw new InvalidOperationException(
                             $"Failed to create admin user '{adminUser.UserName}': {errors}");
-                        throw new InvalidOperationException(
-                            $"Failed to create admin user '{adminUser.UserName}': {errors}");
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs` around lines 108 - 115,
The block handling createResult errors contains a duplicated throw of
InvalidOperationException making the second throw unreachable; remove the second
identical throw so only one InvalidOperationException is thrown after logging
the error. Update the code around createResult, logger.LogError(...,
adminUser.UserName, RoleConstants.Administrator, errors) and the subsequent
throw new InvalidOperationException($"Failed to create admin user
'{adminUser.UserName}': {errors}") to eliminate the duplicate statement.

9-217: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

This file is a minefield of merge/paste artifacts — re-run the entire seeder through a compiler before pushing.

The same class of duplication (extra braces, repeated if conditions, doubled LogError/LogInformation/throw statements) shows up in at least eight distinct places. The current state will not compile, and even if you patch only the syntax errors, the surviving duplicated throws will fail the unreachable-code analyzer. Please run a local build and treat warnings-as-errors before requesting review again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs` around lines 9 - 217,
SeedAsync contains many merge/paste artifacts: remove duplicated braces,
duplicate if checks and repeated log/throw lines so the method compiles and
control flow is correct; specifically inspect the role seeding block
(roleManager.CreateAsync) and the admin/mentor user flows (adminUser,
mentorUser, CreateAsync results, AddToRoleAsync) to eliminate duplicated if
(adminUser is null) / if (mentorUser is null) checks, duplicate
logger.LogError/LogInformation calls and duplicate throw new
InvalidOperationException statements, and restore proper matching braces around
the foreach and method body; after cleaning those symbols (SeedAsync,
roleManager, userManager, adminUser, mentorUser, CreateAsync, AddToRoleAsync)
rebuild locally and enable warnings-as-errors to catch any remaining
unreachable/duplicate code before pushing.
backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs (1)

22-57: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Same "Admin" magic-string issue applies here.

See the note in TestCaseEndpoints.cs: the seeded role is RoleConstants.Administrator ("Administrator"), so every RequireRole("Admin", "Mentor") on lines 22, 27, 37, 42, 47, 52, and 57 will lock administrators out. Replace with RoleConstants.Administrator, RoleConstants.Mentor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs` around lines 22
- 57, Replace the hard-coded "Admin" role string used in RequireAuthorization
calls with the seeded constant RoleConstants.Administrator wherever it's used in
this endpoint file (including the group-level RequireAuthorization and the Map
handlers GetAllExamRooms, GetExamRoomById, UpdateExamRoom, DeleteExamRoom,
AssignExamToRoom, EnrollStudents, and RemoveExamFromRoom); update each
RequireRole("Admin", "Mentor") to RequireRole(RoleConstants.Administrator,
RoleConstants.Mentor) so administrators match the seeded role name.
backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs (2)

29-47: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Same N+1 as GetAllUsersHandler; consolidate the mapping.

Identical per-user GetRolesAsync + GetFileUrlAsync issue as in GetAllUsersHandler. Either share a UserDto mapper between the two handlers or batch both lookups. Currently the two handlers diverge slightly (this one has no try/catch on GetFileUrlAsync), which is exactly the kind of drift duplication causes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs`
around lines 29 - 47, GetAllStudentsHandler is causing N+1 calls by calling
userManager.GetRolesAsync and fileStorageService.GetFileUrlAsync per student;
consolidate this logic with GetAllUsersHandler by extracting a shared mapper
(e.g., IUserMapper or UserDtoFactory) that accepts a list of users and returns
List<UserDto>, or implement batch lookups: fetch all roles for user ids in one
call and resolve all file URLs in bulk before the foreach, then map using the
shared CreateUserDto method. Replace the per-student calls in
GetAllStudentsHandler (and align GetAllUsersHandler) to use the shared mapper or
the batched role/file-url results so both handlers share identical behavior
(including consistent error handling around GetFileUrlAsync).

20-26: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

In-memory pagination after loading every student — does not scale.

GetUsersInRoleAsync("Student") materializes the entire student roster into memory; only then do Skip/Take execute. At any non-trivial student count, the database pulls every row, the API host buffers it, and the per-student N+1 loop (lines 29–31) compounds the damage. Pagination, total count, and role membership must execute in SQL, not in-memory LINQ.

Replace the query with a SQL-side join over AspNetUserRoles/AspNetRoles to push pagination down to the database.

Additionally, "Student" is a magic string. RoleConstants.Student exists—use it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs`
around lines 20 - 26, GetUsersInRoleAsync("Student") pulls all students into
memory and then paginates, causing poor scalability; change
GetAllStudentsHandler to perform pagination and total count in SQL by querying
the user store with a join/filter on AspNetUserRoles/AspNetRoles (or using
UserManager.Users joined to roles) to produce a paged result and a database-side
totalCount, replace the magic string "Student" with RoleConstants.Student, and
eliminate per-student N+1 work by fetching related profile/claims in the same DB
query or in batched queries.
backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllUsersHandler.cs (1)

25-43: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

N+1 pattern plus no resilience around per-user URL resolution.

Two concrete problems with this loop:

  1. N+1 queries. Every page issues PageSize calls to GetRolesAsync and another PageSize calls to GetFileUrlAsync. These run sequentially. Batch the role lookup (single join against UserRoles/Roles) and parallelize or batch the URL resolution.
  2. Inconsistent error handling. GetMeHandler wraps GetFileUrlAsync in try/catch and logs a warning. Here, one failing external storage call aborts the entire page request. Apply the same isolation per user.
♻️ Suggested defensive fix (does not address the N+1, only the resilience gap)
             foreach (var user in users)
             {
                 var roles = await userManager.GetRolesAsync(user);
                 string? profilePicUrl = null;
                 if (!string.IsNullOrWhiteSpace(user.ProfilePictureUrl))
-                    profilePicUrl = await fileStorageService.GetFileUrlAsync(user.ProfilePictureUrl);
+                {
+                    try
+                    {
+                        profilePicUrl = await fileStorageService.GetFileUrlAsync(user.ProfilePictureUrl);
+                    }
+                    catch (Exception ex)
+                    {
+                        logger.LogWarning(ex, "Failed to resolve profile picture URL for user {UserId}", user.Id);
+                    }
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllUsersHandler.cs`
around lines 25 - 43, GetAllUsersHandler currently does per-user sequential
calls to userManager.GetRolesAsync and fileStorageService.GetFileUrlAsync
causing N+1 and brittle failure behavior; fix by (1) batching role resolution:
fetch roles for all user IDs in the page in one query/join (e.g., a repository
call or UserRoles/Role join) and map roles back to each user instead of calling
GetRolesAsync inside the loop, and (2) parallelizing and isolating file URL
resolution: async Task all URL lookups in parallel (e.g., Task.WhenAll) or use
concurrent mapping and wrap each fileStorageService.GetFileUrlAsync call in a
try/catch that logs a warning (same pattern as GetMeHandler) so a single failure
doesn't abort the whole request, then construct each UserDto with mapped roles
and the safely-resolved profilePicUrl.
backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs (1)

72-84: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Protect the curriculum and insight routes as well.

GET /api/courses/{id} now requires authentication, but /curriculum and /insight do not. That leaves an unauthenticated path to course data under the same resource.

Proposed fix
         group.MapGet("/{id:guid}/curriculum", GetCourseCurriculum)
             .WithName("GetCourseCurriculum")
             .WithOpenApi()
+            .RequireAuthorization()
             .Produces<ServiceResponse<List<CurriculumSectionDto>>>()
             .Produces<ServiceResponse<List<CurriculumSectionDto>>>(StatusCodes.Status404NotFound);

         group.MapGet("/{id:guid}/insight", GetCourseInsight)
             .WithName("GetCourseInsight")
             .WithOpenApi()
+            .RequireAuthorization()
             .Produces<ServiceResponse<CourseInsightDto>>()
             .Produces<ServiceResponse<CourseInsightDto>>(StatusCodes.Status404NotFound);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs` around lines 72 -
84, The curriculum and insight endpoints are missing authorization and should be
protected like the main course route; update the group.MapGet calls that
register GetCourseCurriculum and GetCourseInsight to require authentication by
adding the same authorization requirement used elsewhere (e.g., call
.RequireAuthorization() or the project-specific authorization extension) on the
MapGet for both GetCourseCurriculum and GetCourseInsight so these routes enforce
the same auth policy as GET /api/courses/{id}. Ensure you modify the
group.MapGet("/{id:guid}/curriculum", GetCourseCurriculum) and
group.MapGet("/{id:guid}/insight", GetCourseInsight) registrations.
backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs (1)

72-85: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

The migration drops the old join table without preserving existing course-mentor assignments.

Migration 20260510085819_RenameTeacherToMentor.cs drops course_teachers (line 14–15) and creates course_mentors (lines 22–44) with zero attempt to migrate existing data. No INSERT INTO ... SELECT statement copies rows; no manual SQL backfill exists. Every course-teacher assignment in production is deleted on deploy. This is a hard data-loss bug and must be fixed before merging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs`
around lines 72 - 85, The migration drops the old join table and loses all
course-teacher assignments; update the change so data is preserved by either (A)
updating CourseConfiguration/UsingEntity to map the many-to-many to the existing
"course_teachers" table name (so EF will rename the navigation without dropping
data) or (B) add an explicit migration step in
20260510085819_RenameTeacherToMentor that issues raw SQL to copy rows from
course_teachers into course_mentors (INSERT INTO course_mentors SELECT ... FROM
course_teachers) and/or rename the table instead of dropping it; locate the
UsingEntity setup in CourseConfiguration (the HasMany(c =>
c.Mentors).UsingEntity<Dictionary<string, object>> block) and modify the
migration to run safe SQL/data-backfill/rename to preserve existing assignments.
♻️ Duplicate comments (1)
backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs (1)

5-11: 🧹 Nitpick | 🔵 Trivial

Verify ID type consistency and consider immutable collections.

This DTO uses int Id (line 6), which may be inconsistent with the PR's stated goal of migrating to Guid identifiers. See the verification request in RandomQuestionQuery.cs regarding QuestionId type.

Additionally, List<string> Options (line 10) is mutable. Consider IReadOnlyList<string> for better immutability semantics in DTOs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs` around
lines 5 - 11, RandomQuestionResponse currently declares Id as int and Options as
mutable List<string>; update Id to Guid to match the PR’s migration to GUIDs
(ensure consistency with QuestionId in RandomQuestionQuery) and change Options
to an immutable/readonly collection type such as IReadOnlyList<string> (or
IEnumerable<string> if preferred) so the DTO is immutable; update any
callers/serializers that construct or consume RandomQuestionResponse and ensure
using System namespaces for Guid and collection interfaces as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs`:
- Around line 19-20: Remove the duplicate constant declaration for MaxJudgeScore
in the JudgeFromFileCommandHandler class: keep a single private const int
MaxJudgeScore = 100 and delete the redundant line so the type no longer contains
two definitions; after removal, rebuild to ensure all references to
MaxJudgeScore still resolve correctly.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs`:
- Around line 57-58: Replace PII in log messages in AssignMentorsHandler by
removing names/emails and logging non-PII identifiers or counts: where
logger.LogWarning currently logs invalidMentors with string.Join of
FirstName/LastName/Email, change it to log either the count
(invalidMentors.Count) or a list of stable IDs (e.g., invalidMentors.Select(m =>
m.Id)) and update both occurrences (the one at logger.LogWarning around the
first check and the similar block in the retry/validation section at lines
~114-130) to use the same non-PII format, keeping the message text descriptive
(e.g., "Users without Mentor role: count={Count}"/"Invalid mentor IDs: {Ids}").
- Around line 77-137: The current fire-and-forget Task.Run inside
AssignMentorsHandler (and similar in EnrollStudentsHandler) must be replaced by
enqueuing email work to a dedicated hosted background service rather than
running inline; implement a single IHostedService/BackgroundService (similar to
ContainerPoolHostedService/WorkDirSweeperHostedService) that consumes a
concurrent queue or IBackgroundQueue and processes items by calling
emailService.SendCourseAssignmentAsync (and other email methods) with proper
retry/cancellation and logging, then change
AssignMentorsHandler/EnrollStudentsHandler to create and enqueue an email job
object (mentor id, email, names, course id/title/dates, attempt metadata)
instead of calling Task.Run, and remove the inline Task.Run blocks so
request-scoped dependencies are not used by fire-and-forget tasks.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs`:
- Around line 58-64: The handler currently only returns inside the failure
branch after userManager.UpdateAsync in UpdateProfileHandler, leaving the
success path with no return; modify the Handle method so that after a successful
UpdateAsync (when result.Succeeded is true) it returns an appropriate
ServiceResponse<string> indicating success (e.g., with true and a success
message or updated user id), while keeping the existing failure branch and
logger.LogWarning for failures; ensure the method always returns a
ServiceResponse<string> in all code paths.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Question/CheckQuestionAnswerHandler.cs`:
- Around line 34-42: The handler currently always returns question.Explanation
in the CheckAnswerResponse; update CheckQuestionAnswerHandler to conditionally
include the explanation based on the intended assessment policy (for example,
only when the answer is incorrect or when a config/flag like revealExplanation
or assessmentModeAlllowExplanations is true). Locate the construction of
CheckAnswerResponse and change the third argument to either question.Explanation
or null depending on isCorrect and a new/existing setting (e.g.,
revealExplanation or isHighStakes/assessmentMode) so the explanation is withheld
when policy dictates; ensure any new flag is read from the request/context and
propagated into the ServiceResponse flow.
- Around line 15-22: The query in CheckQuestionAnswerHandler currently fetches a
question by ID without respecting visibility/publish rules; update the FindAll
predicate to the same filters used in GetRandomQuestionHandler so hidden
questions and questions from unpublished quizlets are excluded: change the
predicate in the unitOfWork.Repository<Domain.Entities.Question>().FindAll(...)
call to include !q.IsHidden and q.Quizlet.IsPublished (and include the Quizlet
navigation if needed alongside q => q.Answers) so the fetched question is only
returned when it is not hidden and its Quizlet is published, keeping the
existing null check and ServiceResponse behavior.

In `@backend/Src/PIED_LMS.Application/UserCases/Queries/GetMeHandler.cs`:
- Around line 19-20: The handler returns new ServiceResponse<UserDto>(false,
"User not found") without marking it as a NotFound, so ToActionResult will map
it to 400 instead of 404; update the return in GetMeHandler (and any other
handlers signaling missing entities) to set the not-found flag or error code
(e.g., set isNotFound = true or an ErrorCode like "NOT_FOUND") on the
ServiceResponse<UserDto> so the EndpointExtensions.ToActionResult logic will
produce a 404.

In
`@backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorsHandler.cs`:
- Around line 19-21: The current handler materializes all mentors via
_userManager.GetUsersInRoleAsync and then calls AsQueryable(), causing in-memory
filtering; instead build an IQueryable<ApplicationUser> from the identity data
source (e.g., _userManager.Users or the DbContext.Users joined to
AspNetUserRoles filtered by RoleConstants.Mentor) and apply the search
predicate, IsActive filter, CountAsync and pagination (Skip/Take) against that
IQueryable so CountAsync and ToListAsync execute on the database; remove the
in-memory AsQueryable() usage and redundant .ToLower() calls (rely on
case-insensitive collation or use EF functions) and ensure methods in
GetMentorsHandler use async DB-side operations rather than materializing the
full set first.

In
`@backend/Src/PIED_LMS.Application/UserCases/Queries/Question/GetRandomQuestionHandler.cs`:
- Around line 15-18: The current randomization using .OrderBy(q =>
Guid.NewGuid()) on the query built from
unitOfWork.Repository<Domain.Entities.Question>().FindAll(...) does not produce
true SQL-side randomness; replace the client-side Guid approach with a
provider-appropriate method: for SQL Server change the ordering to use
EF.Functions.Random() in the OrderBy call so the database performs random
ordering, otherwise load the filtered set returned by FindAll(...) into memory
(e.g., ToListAsync) and then pick a random element using Random.Shared.Next() or
similar before calling FirstOrDefaultAsync; update the code paths that call
.OrderBy(...) and .FirstOrDefaultAsync(...) accordingly (refer to OrderBy,
FindAll, FirstOrDefaultAsync usage).
- Around line 25-33: GetRandomQuestionHandler currently builds options from
randomQuestion.Answers and may expose correctness; change the handler so it only
projects answer.Content into the RandomQuestionResponse (do not include or
serialize any IsCorrect metadata or the full Answer objects), validate
randomQuestion.Answers is non-empty before constructing the response (throw or
return an error if empty), and randomize/shuffle the options list before passing
it into RandomQuestionResponse to avoid predictable ordering; also review the
RandomQuestionResponse constructor/DTO to ensure it contains only
primitive/display fields (Id, Content, QuestionType, QuizletLevel, options) and
not any IsCorrect or Answer collection references.

In `@backend/Src/PIED_LMS.Contract/Services/Course/Command.cs`:
- Line 4: Remove the duplicate using directive by deleting the redundant "using
PIED_LMS.Contract.Constants;" entry so only a single using
PIED_LMS.Contract.Constants; remains; locate the duplicate among the top-of-file
using statements in the Course/Command.cs file and remove the extra line to
resolve CS0105.

In `@backend/Src/PIED_LMS.Contract/Services/Course/Request.cs`:
- Line 20: Rename the public property Value on the Request class to a
self-descriptive name (e.g., CourseTier, CreditValue or CourseLevel) and update
all references; add an XML doc comment above the property explaining its
purpose; constrain its allowed values using a DataAnnotation (e.g.,
[Range(min,max)] for numeric ranges or replace the int with a strongly typed
enum like CourseLevel) and update the [FromForm] attribute to remain on the
renamed property so model binding continues to work; ensure any callers or
serializer contracts that expect Request.Value are updated to use the new
property name.
- Around line 8-36: Both CreateCourseRequest and UpdateCourseRequest are
identical; collapse duplication by extracting a single shared record (e.g.,
CourseRequest) that contains the common properties and then reuse it for both
operations. Replace the two duplicated record declarations with one shared
record named CourseRequest (containing Title, Description, ThumbnailFile,
StartDate, EndDate, Status, Tags, Slug, Duration, Seats, Price, Value) and then
add lightweight aliases for the API contract such as "using CreateCourseRequest
= CourseRequest;" and "using UpdateCourseRequest = CourseRequest;" (or
alternatively derive UpdateCourseRequest from CourseRequest if you need
different future behavior), ensuring you remove the original CreateCourseRequest
and UpdateCourseRequest declarations.
- Around line 18-20: The Seats and Price form fields in Request.cs are
incorrectly typed as string?; change them to numeric types (e.g., [FromForm]
int? Seats and [FromForm] decimal? Price or non-nullable int and decimal if
required) in the CreateCourseCommand/UpdateCourseCommand request models, update
any validation attributes (e.g., Range, Required) and model binding usage
accordingly, and regenerate/verify OpenAPI/Swagger metadata so the API surface
and consumers receive proper integer/decimal types and validation instead of
freeform strings.

In `@backend/Src/PIED_LMS.Contract/Services/Enrollment/Command.cs`:
- Line 11: The parameter declaration uses a fully qualified type name
Microsoft.AspNetCore.Http.IFormFile even though the file already imports
Microsoft.AspNetCore.Http; update the parameter signature to use the simple type
IFormFile for PaymentProof (the constructor/record/property named PaymentProof
in Command.cs) to remove the redundant namespace qualification.

In `@backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs`:
- Around line 18-22: Change the mutable List<string> to an immutable
IReadOnlyList<string> on the CheckAnswerResponse record to enforce immutability:
update the record signature in RandomQuestionDto.cs from List<string>
CorrectAnswers to IReadOnlyList<string> CorrectAnswers (ensure
System.Collections.Generic is available), and update any call sites that
construct CheckAnswerResponse to pass an IReadOnlyList (e.g., use
myList.AsReadOnly(), myEnumerable.ToList().AsReadOnly(), or an array) so callers
still compile.

In `@backend/Src/PIED_LMS.Domain/Entities/Enrollment.cs`:
- Line 9: The migration 20260510091502_ChangeCourseFromIntegerToGuid is invalid
because it tries to cast integer course IDs to UUIDs; rewrite the migration to
preserve and map existing integer IDs before changing Enrollment.CourseId (and
other FK columns such as in course_mentors and course_prerequisites): add a
temporary int column (e.g., course_id_old) to courses, populate a deterministic
or preserved UUID for each existing course (insert into courses new uuid values
and record mapping), update all referencing tables by joining on course_id_old
to the new UUIDs inside a single transaction (use CTE or JOIN to update
enrollments, course_mentors, course_prerequisites), then remove the temporary
column; include a safe Down() that reverses using the mapping and test the
migration on a staging DB with sample data before merging.

In `@backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs`:
- Around line 143-145: Remove the duplicate logger.LogInformation call that logs
the admin creation success; keep a single call to
logger.LogInformation("Successfully created/ensured admin user: {UserName} with
role: {Role}", adminUser.UserName, RoleConstants.Administrator) (in the
DbInitializer/initialization logic) so the message is only logged once and the
parameters (adminUser.UserName and RoleConstants.Administrator) are preserved.
- Around line 198-205: In the mentor role-assignment branch remove the
duplicated throw so only one InvalidOperationException is thrown; locate the
block that logs via logger.LogError("Failed to assign role {Role} to user
{UserName}. Errors: {Errors}", RoleConstants.Mentor, mentorUser.UserName,
errors) and the subsequent throws that construct new
InvalidOperationException($"Failed to assign role '{RoleConstants.Mentor}' to
user '{mentorUser.UserName}': {errors}"), and delete the repeated/extra throw
statement so execution throws exactly once.
- Around line 99-104: There's a duplicated conditional "if (adminUser is null)"
causing a redundant nested check; remove the extra duplicate so there's a single
if (adminUser is null) block that contains the existing body (build errors from
createResult.Errors and throw the InvalidOperationException with that message).
Ensure you keep the variables adminUser and createResult and the thrown
InvalidOperationException exactly as currently used (i.e., var errors =
string.Join(...); throw new InvalidOperationException(...)).
- Around line 173-178: There is a duplicated conditional check "if (mentorUser
is null)" in the re-fetch block for mentorUser; remove the redundant duplicate
so the block reads a single if (mentorUser is null) { var errors =
string.Join(", ", createResult.Errors.Select(e => e.Description)); throw new
InvalidOperationException($"Mentor user duplicate error but could not re-fetch
user: {errors}"); } — locate the occurrence referencing mentorUser and
createResult in DbInitializer.cs and delete the extra repeated if line.
- Around line 52-54: The file DbInitializer.cs contains two extra closing braces
after the foreach block that break compilation; open the DbInitializer class and
remove the stray closing braces that follow the foreach body (ensure the foreach
in the DbInitializer method ends with a single closing brace and that only the
intended method and class-level braces remain). Locate the foreach in
DbInitializer (the loop initializing DB entries) and delete the superfluous '}'
characters so the method and class brace structure is balanced and compiles.
- Around line 127-135: Remove the duplicated log and throw in the admin
role-assignment block: keep a single logger.LogError call that logs
RoleConstants.Administrator, adminUser.UserName and the errors string (from
roleResult.Errors.Select...), and keep a single throw new
InvalidOperationException(...) that uses the same errors message; delete the
second identical logger.LogError and the second identical throw so only one log
and one exception remain (refer to roleResult, logger.LogError,
RoleConstants.Administrator, adminUser.UserName and the
InvalidOperationException in this block).
- Around line 153-154: The code contains a duplicated check "if (mentorUser is
null)" (the second occurrence repeats the first), creating an unintended nested
conditional; in DbInitializer.cs remove the redundant second "if (mentorUser is
null)" so there is only one null-check for mentorUser and ensure the subsequent
initialization/assignment block for mentorUser remains as the single guarded
body (reference the mentorUser null-check in the DbInitializer initialization
logic).

In `@backend/Src/PIED_LMS.Presentation/APIs/AuthenticationEndpoints.cs`:
- Around line 41-45: The fluent pipeline for registering the ResetPassword
endpoint is broken by an extra semicolon and duplicated fluent calls: locate the
group.MapPost("/reset-password", ResetPassword) chain and remove the stray
semicolon that terminates the expression, then delete the duplicated lines so
the call becomes a single fluent chain using .WithName("ResetPassword") and
.WithServiceResponseOpenApi<string>(ServiceResponseStatusProfile.OkOrBadRequest)
on the same statement; ensure the final expression is a continuous fluent call
(no orphaned leading dots).

In `@backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs`:
- Around line 172-179: The current guard in CourseEndpoints.cs wrongly rejects
an empty MentorIds list and prevents clearing all mentors; change the validation
to only reject a null MentorIds (i.e., remove the Count == 0 check) so an empty
list is accepted as a valid full-replacement request that clears mentors; update
the error message to say "MentorIds must be provided" and keep the check around
request.MentorIds (and any downstream code that applies the replacement should
already handle an empty list).
- Around line 20-28: The OpenAPI response types for the CreateCourse endpoint
are incorrect: the route mapping for CreateCourse advertises
Produces<ServiceResponse<int>> but the handler returns ServiceResponse<Guid>.
Update both Produces<ServiceResponse<int>>(StatusCodes.Status201Created) and
Produces<ServiceResponse<int>>(StatusCodes.Status400BadRequest) to use
ServiceResponse<Guid> so the OpenAPI contract matches the CreateCourse handler's
return type.
- Around line 14-17: The fluent chain that creates the route group using
app.MapGroup("/api/courses").WithName("Courses").WithOpenApi() has a stray
duplicate .WithOpenApi(); on the next line which causes a syntax error; remove
the extra .WithOpenApi(); (the detached call after the statement that assigns
var group) so the code keeps a single .WithOpenApi() in the chain that defines
group (reference: MapGroup("/api/courses"), WithName, WithOpenApi, var group).

In `@backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs`:
- Around line 6-7: There are two identical using alias declarations for
ExamRoomAccessResponse causing compiler error CS1537; remove the duplicate alias
so only one using ExamRoomAccessResponse =
PIED_LMS.Contract.Services.ExamParticipation.ExamRoomAccessResponse; remains in
ExamRoomEndpoints.cs, ensuring no other alias duplicates exist in that file.
- Around line 64-83: The code has duplicated/dangling fluent calls to
WithServiceResponseOpenApi — remove the stray second calls that start with a
leading "." after the semicolon or attach them to the same fluent chain;
specifically, fix the MapGet chains for GetExamsInRoomForStudent and
GetAvailableExamRoomsForStudent (and the earlier PaginatedResponse chain) by
either deleting the duplicated
WithServiceResponseOpenApi<PaginatedResponse<ExamRoomResponse>> and
WithServiceResponseOpenApi<List<ExamInRoomResponse>> lines, or by moving those
second WithServiceResponseOpenApi(...) invocations so they directly follow the
preceding fluent statement (no terminating semicolon) in the MapGet chains that
reference GetExamsInRoomForStudent and GetAvailableExamRoomsForStudent.

In `@backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs`:
- Around line 30-34: The endpoint registration for MapPost("/check",
CheckAnswer) currently calls .DisableAntiforgery() which disables CSRF
protection; remove the .DisableAntiforgery() call from the
group.MapPost(...).WithName("CheckAnswer").WithServiceResponseOpenApi<CheckAnswerResponse>(...)
chain when you add authentication for the CheckAnswer endpoint so that CSRF
protection is enforced for authenticated requests.
- Around line 18-22: The endpoints group currently calls AllowAnonymous() on the
MapGroup("/api/questions") (variable group), exposing question data; remove or
replace AllowAnonymous() with a RequireAuthorization() call on the same group
(e.g., group.RequireAuthorization() or
group.RequireAuthorization("EnrolledStudentPolicy")) to enforce authenticated
access, and optionally wire a specific policy/role (e.g., "Student",
"EnrolledStudentPolicy") to restrict to enrolled users and add middleware for
rate-limiting/anti-abuse if available.

In `@backend/Src/PIED_LMS.Presentation/APIs/QuizletEndpoints.cs`:
- Around line 20-43: Replace the hardcoded role name literals in the endpoint
authorization calls with the RoleConstants fields: where you currently call
RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor")) (used on
MapGet/GetAllQuizlets, MapGet/GetQuizletById, MapDelete/DeleteQuizlet,
MapPut/UpdateQuizlet and similar endpoints), change to
RequireAuthorization(policy => policy.RequireRole(RoleConstants.Administrator,
RoleConstants.Mentor)); update all other occurrences (e.g., in
TestCaseEndpoints, ExamEndpoints, ExamRoomEndpoints) so every RequireRole
invocation uses RoleConstants.Administrator and RoleConstants.Mentor instead of
string literals to centralize role names.
- Around line 94-99: Remove the redundant Enum.IsDefined check and the forced
empty-string for description: compute finalLevel simply as request.Level ??
QuizletLevel.Easy (drop Enum.IsDefined since model binding guarantees valid
nullable enums) and pass request.Description through as-is (nullable) into
CreateQuestionQuizCommand so you don't lose the caller's intent; if the command
requires non-null Description, update the command signature or validation
instead of coercing here.

In `@backend/Src/PIED_LMS.Presentation/APIs/StudentSubmissionEndpoints.cs`:
- Around line 30-34: GetSubmissionById currently only restricts Students to
their own submissions; Mentors/Admins lack resource-based checks. In
GetSubmissionByIdQueryHandler (look for roleClaim, submission.StudentId,
currentUserId and submission.ExamId), add a branch for Mentor/Admin that
validates the user is assigned to the submission's exam (for example call an
existing IExamRepository or IAuthorizationService method like
IsUserAssignedToExam(currentUserId, submission.ExamId) or AuthorizeAsync(user,
submission, "CanViewExamSubmission")). If that check fails return the
appropriate Forbidden/NotAuthorized ServiceResponse; otherwise allow the
response. Ensure you use the existing services/dependencies rather than
bypassing auth logic.

In `@backend/Src/PIED_LMS.Presentation/APIs/TestCaseEndpoints.cs`:
- Around line 17-34: Replace all hardcoded role strings with the RoleConstants
fields: in TestCaseEndpoints (the MapGet/GetTestCasesByExam,
MapPut/UpdateTestCase, MapDelete/DeleteTestCase and the earlier MapPost/Map
methods) change .RequireRole("Admin", "Mentor") to
.RequireRole(RoleConstants.Administrator, RoleConstants.Mentor); do the same in
ExamEndpoints, ExamRoomEndpoints, QuizletEndpoints and any handlers that call
.RequireRole or compare role strings (e.g., AssignExamToRoomHandler,
CheckExamRoomAccessHandler) so they reference RoleConstants.Administrator and
RoleConstants.Mentor (or the exact constant names for Mentor) instead of magic
literals to centralize role values.

In `@backend/Src/PIED_LMS.Presentation/APIs/TestRoomEndpoints.cs`:
- Around line 15-20: The route group currently requires only Mentor role via
RequireAuthorization(new AuthorizeAttribute { Roles = RoleConstants.Mentor }),
which diverges from other endpoints that allow both Administrator and Mentor;
update the authorization to include RoleConstants.Administrator (e.g., use Roles
= $"{RoleConstants.Administrator},{RoleConstants.Mentor}" or the equivalent in
your authorization helper) and adjust the OpenAPI metadata for
CreateTestRoom/.WithDescription to say that both Administrators and Mentors can
create test rooms so the policy and docs are consistent with other endpoints
(look for RoleConstants and the CreateTestRoom route mapping to apply the
change).

---

Outside diff comments:
In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs`:
- Around line 102-110: The return statement in JudgeFromFileCommandHandler
(inside the method handling the judge-from-file flow) is malformed: remove the
duplicated trailing tokens "false, serviceResult.ErrorCode);" that follow the
already-terminated return new ServiceResponse<JudgeResult>(...); call so the
return constructs a single valid ServiceResponse<JudgeResult> and the method
compiles; locate the return that creates ServiceResponse<JudgeResult> and delete
the extra duplicated arguments/text after the semicolon.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs`:
- Around line 29-35: Pre-checking slug uniqueness with
GenerateAndValidateSlugAsync in CreateCourseHandler is racy; add a DB-level
unique constraint on Course.Slug and catch DbUpdateException on save, detecting
unique-violation (implement a helper like
IsSlugUniqueViolation(DbUpdateException ex)) and translate it into the same
slug-conflict ServiceResponse<Guid> with the existing warning log (use
logger.LogWarning with the same message) instead of letting the generic DB error
bubble up; keep the existing pre-check but ensure the catch around the
repository/DbContext save path returns the slug-conflict response when
IsSlugUniqueViolation returns true.
- Around line 42-47: The handler uploads the thumbnail via
fileStorageService.SaveFileAsync into thumbnailPath before persisting the
Course, which can leave orphaned files if AddAsync/CommitAsync fails; update
CreateCourseHandler (where thumbnailPath is set) to wrap the persistence calls
(CourseRepository.AddAsync and UnitOfWork.CommitAsync) in a try/catch and, on
any exception, call fileStorageService.DeleteFileAsync(thumbnailPath,
cancellationToken) (or the appropriate delete method) to remove the uploaded
file, then rethrow the exception; apply the same compensating-delete pattern to
the other upload site around lines 91-106 where a thumbnail/file is uploaded
before DB commit.

In `@backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs`:
- Around line 29-47: GetAllStudentsHandler is causing N+1 calls by calling
userManager.GetRolesAsync and fileStorageService.GetFileUrlAsync per student;
consolidate this logic with GetAllUsersHandler by extracting a shared mapper
(e.g., IUserMapper or UserDtoFactory) that accepts a list of users and returns
List<UserDto>, or implement batch lookups: fetch all roles for user ids in one
call and resolve all file URLs in bulk before the foreach, then map using the
shared CreateUserDto method. Replace the per-student calls in
GetAllStudentsHandler (and align GetAllUsersHandler) to use the shared mapper or
the batched role/file-url results so both handlers share identical behavior
(including consistent error handling around GetFileUrlAsync).
- Around line 20-26: GetUsersInRoleAsync("Student") pulls all students into
memory and then paginates, causing poor scalability; change
GetAllStudentsHandler to perform pagination and total count in SQL by querying
the user store with a join/filter on AspNetUserRoles/AspNetRoles (or using
UserManager.Users joined to roles) to produce a paged result and a database-side
totalCount, replace the magic string "Student" with RoleConstants.Student, and
eliminate per-student N+1 work by fetching related profile/claims in the same DB
query or in batched queries.

In `@backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllUsersHandler.cs`:
- Around line 25-43: GetAllUsersHandler currently does per-user sequential calls
to userManager.GetRolesAsync and fileStorageService.GetFileUrlAsync causing N+1
and brittle failure behavior; fix by (1) batching role resolution: fetch roles
for all user IDs in the page in one query/join (e.g., a repository call or
UserRoles/Role join) and map roles back to each user instead of calling
GetRolesAsync inside the loop, and (2) parallelizing and isolating file URL
resolution: async Task all URL lookups in parallel (e.g., Task.WhenAll) or use
concurrent mapping and wrap each fileStorageService.GetFileUrlAsync call in a
try/catch that logs a warning (same pattern as GetMeHandler) so a single failure
doesn't abort the whole request, then construct each UserDto with mapped roles
and the safely-resolved profilePicUrl.

In `@backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs`:
- Around line 108-115: The block handling createResult errors contains a
duplicated throw of InvalidOperationException making the second throw
unreachable; remove the second identical throw so only one
InvalidOperationException is thrown after logging the error. Update the code
around createResult, logger.LogError(..., adminUser.UserName,
RoleConstants.Administrator, errors) and the subsequent throw new
InvalidOperationException($"Failed to create admin user '{adminUser.UserName}':
{errors}") to eliminate the duplicate statement.
- Around line 9-217: SeedAsync contains many merge/paste artifacts: remove
duplicated braces, duplicate if checks and repeated log/throw lines so the
method compiles and control flow is correct; specifically inspect the role
seeding block (roleManager.CreateAsync) and the admin/mentor user flows
(adminUser, mentorUser, CreateAsync results, AddToRoleAsync) to eliminate
duplicated if (adminUser is null) / if (mentorUser is null) checks, duplicate
logger.LogError/LogInformation calls and duplicate throw new
InvalidOperationException statements, and restore proper matching braces around
the foreach and method body; after cleaning those symbols (SeedAsync,
roleManager, userManager, adminUser, mentorUser, CreateAsync, AddToRoleAsync)
rebuild locally and enable warnings-as-errors to catch any remaining
unreachable/duplicate code before pushing.

In `@backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs`:
- Around line 72-85: The migration drops the old join table and loses all
course-teacher assignments; update the change so data is preserved by either (A)
updating CourseConfiguration/UsingEntity to map the many-to-many to the existing
"course_teachers" table name (so EF will rename the navigation without dropping
data) or (B) add an explicit migration step in
20260510085819_RenameTeacherToMentor that issues raw SQL to copy rows from
course_teachers into course_mentors (INSERT INTO course_mentors SELECT ... FROM
course_teachers) and/or rename the table instead of dropping it; locate the
UsingEntity setup in CourseConfiguration (the HasMany(c =>
c.Mentors).UsingEntity<Dictionary<string, object>> block) and modify the
migration to run safe SQL/data-backfill/rename to preserve existing assignments.

In `@backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs`:
- Around line 72-84: The curriculum and insight endpoints are missing
authorization and should be protected like the main course route; update the
group.MapGet calls that register GetCourseCurriculum and GetCourseInsight to
require authentication by adding the same authorization requirement used
elsewhere (e.g., call .RequireAuthorization() or the project-specific
authorization extension) on the MapGet for both GetCourseCurriculum and
GetCourseInsight so these routes enforce the same auth policy as GET
/api/courses/{id}. Ensure you modify the group.MapGet("/{id:guid}/curriculum",
GetCourseCurriculum) and group.MapGet("/{id:guid}/insight", GetCourseInsight)
registrations.

In `@backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs`:
- Around line 22-57: Replace the hard-coded "Admin" role string used in
RequireAuthorization calls with the seeded constant RoleConstants.Administrator
wherever it's used in this endpoint file (including the group-level
RequireAuthorization and the Map handlers GetAllExamRooms, GetExamRoomById,
UpdateExamRoom, DeleteExamRoom, AssignExamToRoom, EnrollStudents, and
RemoveExamFromRoom); update each RequireRole("Admin", "Mentor") to
RequireRole(RoleConstants.Administrator, RoleConstants.Mentor) so administrators
match the seeded role name.

---

Duplicate comments:
In `@backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs`:
- Around line 5-11: RandomQuestionResponse currently declares Id as int and
Options as mutable List<string>; update Id to Guid to match the PR’s migration
to GUIDs (ensure consistency with QuestionId in RandomQuestionQuery) and change
Options to an immutable/readonly collection type such as IReadOnlyList<string>
(or IEnumerable<string> if preferred) so the DTO is immutable; update any
callers/serializers that construct or consume RandomQuestionResponse and ensure
using System namespaces for Guid and collection interfaces as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac15f252-9ad3-40c1-b3a0-1ef4e6a39ec0

📥 Commits

Reviewing files that changed from the base of the PR and between 0acf645 and 1b0c1b2.

⛔ Files ignored due to path filters (7)
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510085819_RenameTeacherToMentor.Designer.cs is excluded by !**/*.Designer.cs, !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510085819_RenameTeacherToMentor.cs is excluded by !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510091502_ChangeCourseFromIntegerToGuid.Designer.cs is excluded by !**/*.Designer.cs, !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510091502_ChangeCourseFromIntegerToGuid.cs is excluded by !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510102335_ChangeCourseIntegerToGUID.Designer.cs is excluded by !**/*.Designer.cs, !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/20260510102335_ChangeCourseIntegerToGUID.cs is excluded by !**/Migrations/** and included by backend/Src/**
  • backend/Src/PIED_LMS.Persistence/Migrations/PiedLmsDbContextModelSnapshot.cs is excluded by !**/Migrations/** and included by backend/Src/**
📒 Files selected for processing (63)
  • backend/Src/PIED_LMS.Application/Options/CourseManagementSettings.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Compiler/JudgeFromFileCommandHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignTeachersHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/CreateCourseHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Question/CheckQuestionAnswerHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Quiz/UpdateQuestionQuizHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Room/CreateRoomHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCourseByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetCoursesHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Course/GetStudentAvailableCoursesHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllStudentsHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetAllUsersHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetMeHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/GetUserByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetAllMentorsSimpleHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Mentor/GetMentorsHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Question/GetRandomQuestionHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetAllTeachersSimpleHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeacherByIdHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeachersHandler.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Command.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Request.cs
  • backend/Src/PIED_LMS.Contract/Services/Course/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Enrollment/Command.cs
  • backend/Src/PIED_LMS.Contract/Services/Enrollment/Request.cs
  • backend/Src/PIED_LMS.Contract/Services/Enrollment/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/ExamParticipation/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/ExamParticipation/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Identity/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Identity/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Identity/Validators/UpdateProfileValidator.cs
  • backend/Src/PIED_LMS.Contract/Services/Mentor/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Mentor/Response.cs
  • backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionDto.cs
  • backend/Src/PIED_LMS.Contract/Services/Question/RandomQuestionQuery.cs
  • backend/Src/PIED_LMS.Contract/Services/QuestionQuiz/Request.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Response.cs
  • backend/Src/PIED_LMS.Domain/Abstractions/ICourseLockingService.cs
  • backend/Src/PIED_LMS.Domain/Constants/RoleConstants.cs
  • backend/Src/PIED_LMS.Domain/Entities/Course.cs
  • backend/Src/PIED_LMS.Domain/Entities/Enrollment.cs
  • backend/Src/PIED_LMS.Infrastructure/DbInitializer.cs
  • backend/Src/PIED_LMS.Infrastructure/Email/SmtpEmailService.cs
  • backend/Src/PIED_LMS.Persistence/Configurations/CourseConfiguration.cs
  • backend/Src/PIED_LMS.Persistence/Services/CourseLockingService.cs
  • backend/Src/PIED_LMS.Presentation/APIs/AuthenticationEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/CourseEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/EnrollmentEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/ExamEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/ExamParticipationEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/ExamRoomEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/MentorEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/QuestionEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/QuizletEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/StudentSubmissionEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TeacherEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TestCaseEndpoints.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TestRoomEndpoints.cs
💤 Files with no reviewable changes (8)
  • backend/Src/PIED_LMS.Domain/Constants/RoleConstants.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Query.cs
  • backend/Src/PIED_LMS.Contract/Services/Teacher/Response.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeacherByIdHandler.cs
  • backend/Src/PIED_LMS.Presentation/APIs/TeacherEndpoints.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetAllTeachersSimpleHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignTeachersHandler.cs
  • backend/Src/PIED_LMS.Application/UserCases/Queries/Teacher/GetTeachersHandler.cs

Comment on lines +77 to +137
_ = Task.Run(async () =>
{
foreach (var mentor in mentors)
{
if (string.IsNullOrWhiteSpace(mentor.Email))
{
logger.LogWarning(
"Skipping email notification for mentor {MentorId} - email is null or empty",
mentor.Id);
continue;
}

var courseManagementUrl = _courseManagementSettings.GetCourseUrl(course.Id);
var retryAttempts = _courseManagementSettings.EmailRetryAttempts;
var retryDelay = _courseManagementSettings.EmailRetryDelayMs;
var attempt = 0;
var emailSent = false;

while (attempt < retryAttempts && !emailSent)
{
attempt++;
try
{
await emailService.SendCourseAssignmentAsync(
mentor.Email,
$"{mentor.FirstName} {mentor.LastName}",
course.Title,
course.StartDate,
course.EndDate,
courseManagementUrl,
CancellationToken.None);

emailSent = true;

if (attempt > 1)
{
logger.LogInformation(
"Successfully sent course assignment email to mentor {MentorId} ({Email}) for course {CourseId} on attempt {Attempt}",
mentor.Id, mentor.Email, course.Id, attempt);
}
}
catch (Exception ex)
{
if (attempt == retryAttempts)
{
logger.LogError(ex,
"Failed to send course assignment email to mentor {MentorId} ({Email}) for course {CourseId} after {Attempts} attempts",
mentor.Id, mentor.Email, course.Id, retryAttempts);
}
else
{
logger.LogWarning(ex,
"Failed to send course assignment email to mentor {MentorId} ({Email}) for course {CourseId} on attempt {Attempt}. Retrying in {DelayMs}ms",
mentor.Id, mentor.Email, course.Id, attempt, retryDelay);

await Task.Delay(retryDelay, CancellationToken.None);
}
}
}
}
}, CancellationToken.None);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the codebase already has background job infrastructure that should be used here.
rg -n -C2 'BackgroundService|IHostedService|IBackgroundTaskQueue|Channel<|Task.Run\(' backend/Src

Repository: PiedTeam/pied-lms

Length of output: 2946


🏁 Script executed:

#!/bin/bash
# Search for background task queue abstractions and outbox patterns
rg -n 'IBackgroundTaskQueue|IMessageQueue|IOutbox|IEmailQueue|IJobQueue' backend/Src

Repository: PiedTeam/pied-lms

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check how email service is configured and used
rg -n 'SendCourseAssignmentAsync|emailService|IEmailService' backend/Src -A 2 | head -50

Repository: PiedTeam/pied-lms

Length of output: 5420


🏁 Script executed:

#!/bin/bash
# Check DI registration to see how background services are wired up
rg -n 'AddHostedService|AddSingleton.*Service|AddTransient.*Service' backend/Src --type cs | head -30

Repository: PiedTeam/pied-lms

Length of output: 558


🏁 Script executed:

#!/bin/bash
# Find IEmailService implementation
find backend/Src -name "*.cs" -exec grep -l "interface IEmailService\|class.*IEmailService" {} \;

Repository: PiedTeam/pied-lms

Length of output: 189


🏁 Script executed:

#!/bin/bash
# Check EnrollStudentsHandler to see if it has the same pattern
sed -n '85,95p' backend/Src/PIED_LMS.Application/UserCases/Commands/ExamRoom/EnrollStudentsHandler.cs

Repository: PiedTeam/pied-lms

Length of output: 505


🏁 Script executed:

#!/bin/bash
# Search for any outbox pattern or event publishing
rg -n 'Outbox|IEventPublisher|PublishAsync|AddToOutbox' backend/Src

Repository: PiedTeam/pied-lms

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check IEmailService interface
cat backend/Src/PIED_LMS.Contract/Abstractions/Email/IEmailService.cs

Repository: PiedTeam/pied-lms

Length of output: 801


🏁 Script executed:

#!/bin/bash
# Check SmtpEmailService implementation
cat backend/Src/PIED_LMS.Infrastructure/Email/SmtpEmailService.cs

Repository: PiedTeam/pied-lms

Length of output: 11958


Replace fire-and-forget Task.Run with proper background job processing.

Email delivery must not execute on request-owned dependencies. Direct Task.Run work is lost on application recycle, failures are unobserved, and cancellation is ignored. The codebase already implements BackgroundService and IHostedService patterns (see ContainerPoolHostedService, WorkDirSweeperHostedService). Apply the same pattern here: queue email delivery through a hosted background service, not inline on the request path.

This issue exists systematically across multiple handlers (AssignMentorsHandler, EnrollStudentsHandler). Create a single email background service and eliminate all fire-and-forget email patterns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Course/AssignMentorsHandler.cs`
around lines 77 - 137, The current fire-and-forget Task.Run inside
AssignMentorsHandler (and similar in EnrollStudentsHandler) must be replaced by
enqueuing email work to a dedicated hosted background service rather than
running inline; implement a single IHostedService/BackgroundService (similar to
ContainerPoolHostedService/WorkDirSweeperHostedService) that consumes a
concurrent queue or IBackgroundQueue and processes items by calling
emailService.SendCourseAssignmentAsync (and other email methods) with proper
retry/cancellation and logging, then change
AssignMentorsHandler/EnrollStudentsHandler to create and enqueue an email job
object (mentor id, email, names, course id/title/dates, attempt metadata)
instead of calling Task.Run, and remove the inline Task.Run blocks so
request-scoped dependencies are not used by fire-and-forget tasks.

Comment on lines +15 to +22
var question = await unitOfWork.Repository<Domain.Entities.Question>()
.FindAll(q => q.Id == request.QuestionId, q => q.Answers)
.FirstOrDefaultAsync(cancellationToken);

if (question == null)
{
return new ServiceResponse<CheckAnswerResponse>(false, "Question not found", null, null, true, "NOT_FOUND");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing validation: hidden questions and unpublished quizlets.

The handler fetches any question by ID without checking IsHidden or Quizlet.IsPublished status. This allows users to check answers for hidden questions or questions from unpublished quizlets, which contradicts the filtering in GetRandomQuestionHandler.

Add the same filters used in the random question query:

🔒 Proposed fix to add validation
 var question = await unitOfWork.Repository<Domain.Entities.Question>()
-    .FindAll(q => q.Id == request.QuestionId, q => q.Answers)
+    .FindAll(q => q.Id == request.QuestionId && !q.IsHidden && q.Quizlet.IsPublished, 
+             q => q.Answers, q => q.Quizlet)
     .FirstOrDefaultAsync(cancellationToken);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var question = await unitOfWork.Repository<Domain.Entities.Question>()
.FindAll(q => q.Id == request.QuestionId, q => q.Answers)
.FirstOrDefaultAsync(cancellationToken);
if (question == null)
{
return new ServiceResponse<CheckAnswerResponse>(false, "Question not found", null, null, true, "NOT_FOUND");
}
var question = await unitOfWork.Repository<Domain.Entities.Question>()
.FindAll(q => q.Id == request.QuestionId && !q.IsHidden && q.Quizlet.IsPublished,
q => q.Answers, q => q.Quizlet)
.FirstOrDefaultAsync(cancellationToken);
if (question == null)
{
return new ServiceResponse<CheckAnswerResponse>(false, "Question not found", null, null, true, "NOT_FOUND");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Question/CheckQuestionAnswerHandler.cs`
around lines 15 - 22, The query in CheckQuestionAnswerHandler currently fetches
a question by ID without respecting visibility/publish rules; update the FindAll
predicate to the same filters used in GetRandomQuestionHandler so hidden
questions and questions from unpublished quizlets are excluded: change the
predicate in the unitOfWork.Repository<Domain.Entities.Question>().FindAll(...)
call to include !q.IsHidden and q.Quizlet.IsPublished (and include the Quizlet
navigation if needed alongside q => q.Answers) so the fetched question is only
returned when it is not hidden and its Quizlet is published, keeping the
existing null check and ServiceResponse behavior.

Comment on lines +20 to +43
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"));

// GET /api/quizlets (Admin, Mentor, Teacher — all quizlets summary)
// GET /api/quizlets (Admin, Mentor — all quizlets summary)
group.MapGet("", GetAllQuizlets)
.WithName("GetAllQuizlets")
.WithServiceResponseOpenApi<List<QuizletSummaryResponse>>(ServiceResponseStatusProfile.OkOrBadRequest)
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"));
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"));

group.MapGet("/{id:int}", GetQuizletById)
.WithName("GetQuizletById")
.WithServiceResponseOpenApi<QuizletDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound)
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"));
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"));

// DELETE /api/quizlets/{id}
group.MapDelete("/{id}", DeleteQuizlet)
.WithName("DeleteQuizlet")
.WithServiceResponseOpenApi<string>(ServiceResponseStatusProfile.OkOrBadRequest)
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"));
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"));

// PUT /api/quizlets/{id}
group.MapPut("/{id}", UpdateQuizlet)
.WithName("UpdateQuizlet")
.WithServiceResponseOpenApi<string>(ServiceResponseStatusProfile.OkOrBadRequest)
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"));
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the canonical string value of RoleConstants.Administrator and Mentor.
rg -nP -C2 'class\s+RoleConstants'
rg -nP 'public\s+(const|static\s+readonly)\s+string\s+(Administrator|Mentor|Student)\b\s*=\s*"[^"]+"'
# Also surface other endpoints that hard-code role names.
rg -nP -C1 'RequireRole\s*\(\s*"(Admin|Administrator|Mentor|Student|Teacher)"'

Repository: PiedTeam/pied-lms

Length of output: 11457


Replace hardcoded role strings with RoleConstants constants across all endpoints.

Lines 20, 26, 31, 37, 43 use RequireRole("Admin", "Mentor") as literal strings. These match RoleConstants.Administrator = "Admin" correctly — no authorization bypass exists. However, hardcoding role names instead of referencing the constant creates unnecessary maintenance risk. If role naming conventions change, these scattered literals must be hunted down individually; the constant would handle it centrally.

Use RoleConstants.Administrator and RoleConstants.Mentor throughout. This pattern appears across TestCaseEndpoints.cs, ExamEndpoints.cs, ExamRoomEndpoints.cs, and others — a systematic refactor is warranted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/QuizletEndpoints.cs` around lines 20 -
43, Replace the hardcoded role name literals in the endpoint authorization calls
with the RoleConstants fields: where you currently call
RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor")) (used on
MapGet/GetAllQuizlets, MapGet/GetQuizletById, MapDelete/DeleteQuizlet,
MapPut/UpdateQuizlet and similar endpoints), change to
RequireAuthorization(policy => policy.RequireRole(RoleConstants.Administrator,
RoleConstants.Mentor)); update all other occurrences (e.g., in
TestCaseEndpoints, ExamEndpoints, ExamRoomEndpoints) so every RequireRole
invocation uses RoleConstants.Administrator and RoleConstants.Mentor instead of
string literals to centralize role names.

Comment on lines +94 to +99
var finalLevel = request.Level.HasValue && Enum.IsDefined(typeof(QuizletLevel), request.Level.Value)
? request.Level.Value
: QuizletLevel.Easy;

var command = new CreateQuestionQuizCommand(title, description ?? string.Empty, isPublished, isHidden,
finalLevel, listQuestion);
var command = new CreateQuestionQuizCommand(request.Title, request.Description ?? string.Empty, request.IsPublished, request.IsHidden,
finalLevel, request.ListQuestion);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Enum.IsDefined on a typed nullable enum is theatre.

request.Level is QuizletLevel?. Once the model binder produces a QuizletLevel? value, you cannot smuggle an undefined integer through it — invalid form values fail binding before reaching this code. The Enum.IsDefined check therefore guards an unreachable branch. Drop it and use a plain null-coalesce.

Also, silently coercing Description to string.Empty loses the caller's intent. If the command requires non-null, document that; otherwise pass null through.

♻️ Simplification
-        var finalLevel = request.Level.HasValue && Enum.IsDefined(typeof(QuizletLevel), request.Level.Value)
-            ? request.Level.Value
-            : QuizletLevel.Easy;
-
-        var command = new CreateQuestionQuizCommand(request.Title, request.Description ?? string.Empty, request.IsPublished, request.IsHidden,
-            finalLevel, request.ListQuestion);
+        var command = new CreateQuestionQuizCommand(
+            request.Title,
+            request.Description ?? string.Empty,
+            request.IsPublished,
+            request.IsHidden,
+            request.Level ?? QuizletLevel.Easy,
+            request.ListQuestion);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var finalLevel = request.Level.HasValue && Enum.IsDefined(typeof(QuizletLevel), request.Level.Value)
? request.Level.Value
: QuizletLevel.Easy;
var command = new CreateQuestionQuizCommand(title, description ?? string.Empty, isPublished, isHidden,
finalLevel, listQuestion);
var command = new CreateQuestionQuizCommand(request.Title, request.Description ?? string.Empty, request.IsPublished, request.IsHidden,
finalLevel, request.ListQuestion);
var command = new CreateQuestionQuizCommand(
request.Title,
request.Description ?? string.Empty,
request.IsPublished,
request.IsHidden,
request.Level ?? QuizletLevel.Easy,
request.ListQuestion);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/QuizletEndpoints.cs` around lines 94 -
99, Remove the redundant Enum.IsDefined check and the forced empty-string for
description: compute finalLevel simply as request.Level ?? QuizletLevel.Easy
(drop Enum.IsDefined since model binding guarantees valid nullable enums) and
pass request.Description through as-is (nullable) into CreateQuestionQuizCommand
so you don't lose the caller's intent; if the command requires non-null
Description, update the command signature or validation instead of coercing
here.

Comment on lines +30 to +34
// Admins/Mentors could also use this if they have the ID
group.MapGet("/submissions/{id:guid}", GetSubmissionById)
.WithName("GetSubmissionById")
.RequireAuthorization() // general auth since both teacher and student can view
.WithServiceResponseOpenApi<
SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
.RequireAuthorization() // general auth since both mentor and student can view
.WithServiceResponseOpenApi<SubmissionDetailResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify authorization logic in GetSubmissionByIdQuery handler

# Find the handler implementation
ast-grep --pattern 'class $_ : IRequestHandler<GetSubmissionByIdQuery, $_> {
  $$$
}'

# Also search for authorization checks within the handler
rg -nP -A10 -B2 'GetSubmissionByIdQuery' --glob '*Handler.cs' --glob '*Handlers.cs'

# Check for resource-based authorization or user validation
rg -nP 'GetSubmissionByIdQuery|GetSubmissionById' --glob '*.cs' -C5

Repository: PiedTeam/pied-lms

Length of output: 9572


🏁 Script executed:

cat -n backend/Src/PIED_LMS.Application/UserCases/Queries/Submission/GetSubmissionByIdQueryHandler.cs

Repository: PiedTeam/pied-lms

Length of output: 3044


Incomplete authorization for Mentor/Admin roles.

The handler correctly enforces authentication and restricts Students to viewing only their own submissions (lines 35-37 of GetSubmissionByIdQueryHandler.cs). However, the authorization for Mentor/Admin roles is incomplete: these users can access any submission without verification that they have permission to view it.

The authorization check if (roleClaim == "Student" && submission.StudentId != currentUserId) (line 35) applies only to Students. For all other authenticated users (Mentor, Admin), no resource-based authorization occurs—they can view any submission regardless of course/exam assignment.

Ensure Mentors/Admins are validated against the submission's exam before granting access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/StudentSubmissionEndpoints.cs` around
lines 30 - 34, GetSubmissionById currently only restricts Students to their own
submissions; Mentors/Admins lack resource-based checks. In
GetSubmissionByIdQueryHandler (look for roleClaim, submission.StudentId,
currentUserId and submission.ExamId), add a branch for Mentor/Admin that
validates the user is assigned to the submission's exam (for example call an
existing IExamRepository or IAuthorizationService method like
IsUserAssignedToExam(currentUserId, submission.ExamId) or AuthorizeAsync(user,
submission, "CanViewExamSubmission")). If that check fails return the
appropriate Forbidden/NotAuthorized ServiceResponse; otherwise allow the
response. Ensure you use the existing services/dependencies rather than
bypassing auth logic.

Comment on lines +17 to +34
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
.WithServiceResponseOpenApi<TestCaseResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrForbiddenOrNotFound);
// GET /api/testcases/{examId}
group.MapGet("/{examId:guid}", GetTestCasesByExam)
.WithName("GetTestCasesByExam")
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"))
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
.WithServiceResponseOpenApi<List<TestCaseResponse>>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);

// PUT /api/testcases/{testcaseId}
group.MapPut("/{testcaseId:guid}", UpdateTestCase)
.WithName("UpdateTestCase")
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"))
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
.WithServiceResponseOpenApi<TestCaseResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);

// DELETE /api/testcases/{testcaseId}
group.MapDelete("/{testcaseId:guid}", DeleteTestCase)
.WithName("DeleteTestCase")
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"))
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the actual string value of RoleConstants.Administrator vs. usages of "Admin".
fd -e cs RoleConstants
rg -nP --type=cs 'public\s+const\s+string\s+(Administrator|Admin|Mentor|Teacher)\s*='
rg -nP --type=cs 'RequireRole\(' -C1
rg -nP --type=cs '"Admin"' -C1

Repository: PiedTeam/pied-lms

Length of output: 26972


Use constants instead of magic strings for role checks throughout the codebase.

While RoleConstants.Administrator correctly resolves to "Admin", the current codebase inconsistently mixes magic strings and constants for role authorization. TestCaseEndpoints, ExamEndpoints, ExamRoomEndpoints, and QuizletEndpoints all hardcode "Admin" and "Mentor" in .RequireRole() calls, whereas EnrollmentEndpoints, CourseEndpoints, and AdminEndpoints correctly use RoleConstants.Administrator. Additionally, role checks in handler classes like AssignExamToRoomHandler and CheckExamRoomAccessHandler use magic strings.

Replace all occurrences with the corresponding constants to maintain a single source of truth and eliminate silent failures if role values ever change.

Example fix for TestCaseEndpoints.cs
-            .RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
+            .RequireAuthorization(policy =>
+                policy.RequireRole(RoleConstants.Administrator, RoleConstants.Mentor))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
.WithServiceResponseOpenApi<TestCaseResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrForbiddenOrNotFound);
// GET /api/testcases/{examId}
group.MapGet("/{examId:guid}", GetTestCasesByExam)
.WithName("GetTestCasesByExam")
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"))
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
.WithServiceResponseOpenApi<List<TestCaseResponse>>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
// PUT /api/testcases/{testcaseId}
group.MapPut("/{testcaseId:guid}", UpdateTestCase)
.WithName("UpdateTestCase")
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"))
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
.WithServiceResponseOpenApi<TestCaseResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
// DELETE /api/testcases/{testcaseId}
group.MapDelete("/{testcaseId:guid}", DeleteTestCase)
.WithName("DeleteTestCase")
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor", "Teacher"))
.RequireAuthorization(policy => policy.RequireRole("Admin", "Mentor"))
.RequireAuthorization(policy =>
policy.RequireRole(RoleConstants.Administrator, RoleConstants.Mentor))
.WithServiceResponseOpenApi<TestCaseResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrForbiddenOrNotFound);
// GET /api/testcases/{examId}
group.MapGet("/{examId:guid}", GetTestCasesByExam)
.WithName("GetTestCasesByExam")
.RequireAuthorization(policy =>
policy.RequireRole(RoleConstants.Administrator, RoleConstants.Mentor))
.WithServiceResponseOpenApi<List<TestCaseResponse>>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
// PUT /api/testcases/{testcaseId}
group.MapPut("/{testcaseId:guid}", UpdateTestCase)
.WithName("UpdateTestCase")
.RequireAuthorization(policy =>
policy.RequireRole(RoleConstants.Administrator, RoleConstants.Mentor))
.WithServiceResponseOpenApi<TestCaseResponse>(ServiceResponseStatusProfile.OkOrBadRequestOrNotFound);
// DELETE /api/testcases/{testcaseId}
group.MapDelete("/{testcaseId:guid}", DeleteTestCase)
.WithName("DeleteTestCase")
.RequireAuthorization(policy =>
policy.RequireRole(RoleConstants.Administrator, RoleConstants.Mentor))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/TestCaseEndpoints.cs` around lines 17
- 34, Replace all hardcoded role strings with the RoleConstants fields: in
TestCaseEndpoints (the MapGet/GetTestCasesByExam, MapPut/UpdateTestCase,
MapDelete/DeleteTestCase and the earlier MapPost/Map methods) change
.RequireRole("Admin", "Mentor") to .RequireRole(RoleConstants.Administrator,
RoleConstants.Mentor); do the same in ExamEndpoints, ExamRoomEndpoints,
QuizletEndpoints and any handlers that call .RequireRole or compare role strings
(e.g., AssignExamToRoomHandler, CheckExamRoomAccessHandler) so they reference
RoleConstants.Administrator and RoleConstants.Mentor (or the exact constant
names for Mentor) instead of magic literals to centralize role values.

Comment on lines +15 to +20
.RequireAuthorization(new AuthorizeAttribute { Roles = RoleConstants.Mentor });

group.MapPost("/", CreateTestRoom)
.WithName("CreateTestRoom")
.WithSummary("Create a new test room")
.WithDescription("Creates a new test room. Only teachers can create test rooms.")
.WithDescription("Creates a new test room. Only mentors can create test rooms.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Authorization excludes Administrator — inconsistent with every other endpoint in this PR.

Every other migrated endpoint (ExamEndpoints, ExamRoomEndpoints, TestCaseEndpoints, ExamParticipationEndpoints) grants access to both Administrator and Mentor. This route group restricts to Mentor alone, locking out admins. Confirm whether that is intentional; if not, widen the policy and update the OpenAPI description accordingly.

🛠️ Suggested fix
-            .RequireAuthorization(new AuthorizeAttribute { Roles = RoleConstants.Mentor });
+            .RequireAuthorization(new AuthorizeAttribute
+            {
+                Roles = $"{RoleConstants.Administrator},{RoleConstants.Mentor}"
+            });

         group.MapPost("/", CreateTestRoom)
             .WithName("CreateTestRoom")
             .WithSummary("Create a new test room")
-            .WithDescription("Creates a new test room. Only mentors can create test rooms.")
+            .WithDescription("Creates a new test room. Only administrators and mentors can create test rooms.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.RequireAuthorization(new AuthorizeAttribute { Roles = RoleConstants.Mentor });
group.MapPost("/", CreateTestRoom)
.WithName("CreateTestRoom")
.WithSummary("Create a new test room")
.WithDescription("Creates a new test room. Only teachers can create test rooms.")
.WithDescription("Creates a new test room. Only mentors can create test rooms.")
.RequireAuthorization(new AuthorizeAttribute
{
Roles = $"{RoleConstants.Administrator},{RoleConstants.Mentor}"
});
group.MapPost("/", CreateTestRoom)
.WithName("CreateTestRoom")
.WithSummary("Create a new test room")
.WithDescription("Creates a new test room. Only administrators and mentors can create test rooms.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Src/PIED_LMS.Presentation/APIs/TestRoomEndpoints.cs` around lines 15
- 20, The route group currently requires only Mentor role via
RequireAuthorization(new AuthorizeAttribute { Roles = RoleConstants.Mentor }),
which diverges from other endpoints that allow both Administrator and Mentor;
update the authorization to include RoleConstants.Administrator (e.g., use Roles
= $"{RoleConstants.Administrator},{RoleConstants.Mentor}" or the equivalent in
your authorization helper) and adjust the OpenAPI metadata for
CreateTestRoom/.WithDescription to say that both Administrators and Mentors can
create test rooms so the policy and docs are consistent with other endpoints
(look for RoleConstants and the CreateTestRoom route mapping to apply the
change).

throw new InvalidOperationException(
$"Failed to create teacher user '{teacherUser.UserName}': {errors}");
logger.LogInformation("Admin user already in role: {UserName} / {Role}",
adminUser.UserName, RoleConstants.Administrator);
logger.LogInformation("Successfully created/ensured teacher user: {UserName} with role: {Role}",
teacherUser.UserName, RoleConstants.Teacher);
logger.LogInformation("Successfully created/ensured admin user: {UserName} with role: {Role}",
adminUser.UserName, RoleConstants.Administrator);
throw new InvalidOperationException(
$"Failed to create teacher user '{mentorUser.UserName}': {errors}");
logger.LogError("Failed to create mentor user {UserName} with role {Role}. Errors: {Errors}",
mentorUser.UserName, RoleConstants.Mentor, errors);
else
{
logger.LogInformation("Mentor user already in role: {UserName} / {Role}",
mentorUser.UserName, RoleConstants.Mentor);
Comment on lines +118 to +134
catch (Exception ex)
{
if (attempt == retryAttempts)
{
logger.LogError(ex,
"Failed to send course assignment email to mentor {MentorId} ({Email}) for course {CourseId} after {Attempts} attempts",
mentor.Id, mentor.Email, course.Id, retryAttempts);
}
else
{
logger.LogWarning(ex,
"Failed to send course assignment email to mentor {MentorId} ({Email}) for course {CourseId} on attempt {Attempt}. Retrying in {DelayMs}ms",
mentor.Id, mentor.Email, course.Id, attempt, retryDelay);

await Task.Delay(retryDelay, CancellationToken.None);
}
}
Comment on lines +141 to +146
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error occurred while assigning mentors to course {CourseId}",
request.CourseId);
return new ServiceResponse<string>(false, "An unexpected error occurred while assigning mentors");
}
Comment on lines +31 to +34
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to resolve profile picture URL for user {UserId}. Key: {Key}", user.Id, user.ProfilePictureUrl);
}
@KietPham-VN KietPham-VN merged commit 099f5de into main May 11, 2026
7 checks passed
@KietPham-VN KietPham-VN deleted the feat/fix-course-authen branch May 11, 2026 00:52
@coderabbitai coderabbitai Bot mentioned this pull request May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants