diff --git a/SpeakingInBitsWeb/Controllers/CoursesController.cs b/SpeakingInBitsWeb/Controllers/CoursesController.cs new file mode 100644 index 0000000..9de72a3 --- /dev/null +++ b/SpeakingInBitsWeb/Controllers/CoursesController.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SpeakingInBitsWeb.Data; +using SpeakingInBitsWeb.Models; + +namespace SpeakingInBitsWeb.Controllers; + +[Authorize(Roles = Roles.Instructor)] +public class CoursesController : Controller +{ + private readonly ApplicationDbContext _context; + private readonly UserManager _userManager; + + public CoursesController(ApplicationDbContext context, UserManager userManager) + { + _context = context; + _userManager = userManager; + } + + // GET: Courses + public async Task Index() + { + string? userId = _userManager.GetUserId(User); + var courses = await _context.Courses + .Where(c => c.CourseInstructor != null && c.CourseInstructor.Id == userId) + .ToListAsync(); + return View(courses); + } + + // GET: Courses/Create + public IActionResult Create() + { + return View(); + } + + // POST: Courses/Create + // To protect from overposting attacks, enable the specific properties you want to bind to. + // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(Course course) + { + string? userId = _userManager.GetUserId(User); + Instructor? instructor = await _context.Users.OfType() + .FirstOrDefaultAsync(i => i.Id == userId); + + if (instructor != null) + { + course.CourseInstructor = instructor; + } + + // Remove CourseInstructor from ModelState since it's being set here programmatically + // It must be removed because it's happening after model binding and validation + ModelState.Remove(nameof(Course.CourseInstructor)); + + if (ModelState.IsValid) + { + _context.Add(course); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + return View(course); + } + + // GET: Courses/Edit/5 + public async Task Edit(int? id) + { + if (id == null) + { + return NotFound(); + } + + + + // Fetch the course from the database, including the instructor + var courseToUpdate = await _context.Courses + .Include(c => c.CourseInstructor) + .FirstOrDefaultAsync(c => c.Id == id); + + if (courseToUpdate == null) + { + return NotFound(); + } + + // Get current user ID to check ownership + string? userId = _userManager.GetUserId(User); + if (courseToUpdate.CourseInstructor == null || courseToUpdate.CourseInstructor.Id != userId) + { + return Forbid(); + } + + return View(courseToUpdate); + } + + // POST: Courses/Edit/5 + // To protect from overposting attacks, enable the specific properties you want to bind to. + // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, Course course) + { + if (id != course.Id) + { + return NotFound(); + } + + // Remove CourseInstructor from ModelState since we're not updating it + ModelState.Remove(nameof(Course.CourseInstructor)); + + if (ModelState.IsValid) + { + // Get current user ID + string? userId = _userManager.GetUserId(User); + + // Fetch the course from the database, including the instructor + var courseToUpdate = await _context.Courses + .Include(c => c.CourseInstructor) + .FirstOrDefaultAsync(c => c.Id == id); + + if (courseToUpdate == null) + { + return NotFound(); + } + + // Check ownership + if (courseToUpdate.CourseInstructor == null || courseToUpdate.CourseInstructor.Id != userId) + { + return Forbid(); + } + + // Update allowed properties only (do not update instructor) + courseToUpdate.Title = course.Title; + courseToUpdate.Description = course.Description; + courseToUpdate.CourseCode = course.CourseCode; + + await _context.SaveChangesAsync(); + + return RedirectToAction(nameof(Index)); + } + return View(course); + } + + // GET: Courses/Delete/5 + public async Task Delete(int? id) + { + if (id == null) + { + return NotFound(); + } + + // Get current user ID + string? userId = _userManager.GetUserId(User); + + // Fetch the course from the database, including the instructor + var courseToDelete = await _context.Courses + .Include(c => c.CourseInstructor) + .FirstOrDefaultAsync(c => c.Id == id); + + if (courseToDelete == null) + { + return NotFound(); + } + + // Check ownership + if (courseToDelete.CourseInstructor == null || courseToDelete.CourseInstructor.Id != userId) + { + return Forbid(); + } + + return View(courseToDelete); + } + + // POST: Courses/Delete/5 + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + // Get current user ID + string? userId = _userManager.GetUserId(User); + + // Fetch the course from the database, including the instructor + var courseToDelete = await _context.Courses + .Include(c => c.CourseInstructor) + .FirstOrDefaultAsync(c => c.Id == id); + + if (courseToDelete == null) + { + return NotFound(); + } + + // Check ownership + if (courseToDelete.CourseInstructor == null || courseToDelete.CourseInstructor.Id != userId) + { + return Forbid(); + } + + _context.Courses.Remove(courseToDelete); + await _context.SaveChangesAsync(); + + return RedirectToAction(nameof(Index)); + } +} diff --git a/SpeakingInBitsWeb/Models/Roles.cs b/SpeakingInBitsWeb/Models/Roles.cs new file mode 100644 index 0000000..ca0ba1c --- /dev/null +++ b/SpeakingInBitsWeb/Models/Roles.cs @@ -0,0 +1,17 @@ +namespace SpeakingInBitsWeb.Models; + +/// +/// Defines constant values for application roles. +/// +public static class Roles +{ + /// + /// Role name for instructors who can manage courses. + /// + public const string Instructor = "Instructor"; + + /// + /// Role name for students who can enroll in courses. + /// + public const string Student = "Student"; +} diff --git a/SpeakingInBitsWeb/Models/SeedData.cs b/SpeakingInBitsWeb/Models/SeedData.cs index 61db0f2..82dc804 100644 --- a/SpeakingInBitsWeb/Models/SeedData.cs +++ b/SpeakingInBitsWeb/Models/SeedData.cs @@ -13,7 +13,7 @@ public static class SeedData /// The role manager service. public static async Task CreateRolesAsync(RoleManager roleManager) { - string[] roles = ["Instructor", "Student"]; + string[] roles = [Roles.Instructor, Roles.Student]; foreach (var role in roles) { @@ -35,7 +35,7 @@ public static async Task CreateDefaultUserAsync(UserManager use if (defaultUser == null) { - ApplicationUser newUser = new() + Instructor newUser = new() { UserName = "DefaultInstructor", Email = defaultEmail, @@ -49,7 +49,7 @@ public static async Task CreateDefaultUserAsync(UserManager use if (result.Succeeded) { - await userManager.AddToRoleAsync(newUser, "Instructor"); + await userManager.AddToRoleAsync(newUser, Roles.Instructor); } } } diff --git a/SpeakingInBitsWeb/SpeakingInBitsWeb.csproj b/SpeakingInBitsWeb/SpeakingInBitsWeb.csproj index c2a5e4b..5477e23 100644 --- a/SpeakingInBitsWeb/SpeakingInBitsWeb.csproj +++ b/SpeakingInBitsWeb/SpeakingInBitsWeb.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -8,13 +8,25 @@ - - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + + + all + + + + + diff --git a/SpeakingInBitsWeb/Views/Courses/Create.cshtml b/SpeakingInBitsWeb/Views/Courses/Create.cshtml new file mode 100644 index 0000000..f45472b --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Create.cshtml @@ -0,0 +1,43 @@ +@model SpeakingInBitsWeb.Models.Course + +@{ + ViewData["Title"] = "Create"; +} + +

Create

+ +

Course

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/SpeakingInBitsWeb/Views/Courses/Delete.cshtml b/SpeakingInBitsWeb/Views/Courses/Delete.cshtml new file mode 100644 index 0000000..8bce683 --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Delete.cshtml @@ -0,0 +1,39 @@ +@model SpeakingInBitsWeb.Models.Course + +@{ + ViewData["Title"] = "Delete"; +} + +

Delete

+ +

Are you sure you want to delete this?

+
+

Course

+
+
+
+ @Html.DisplayNameFor(model => model.Title) +
+
+ @Html.DisplayFor(model => model.Title) +
+
+ @Html.DisplayNameFor(model => model.CourseCode) +
+
+ @Html.DisplayFor(model => model.CourseCode) +
+
+ @Html.DisplayNameFor(model => model.Description) +
+
+ @Html.DisplayFor(model => model.Description) +
+
+ +
+ + | + Back to List +
+
diff --git a/SpeakingInBitsWeb/Views/Courses/Edit.cshtml b/SpeakingInBitsWeb/Views/Courses/Edit.cshtml new file mode 100644 index 0000000..7efd64d --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Edit.cshtml @@ -0,0 +1,44 @@ +@model SpeakingInBitsWeb.Models.Course + +@{ + ViewData["Title"] = "Edit"; +} + +

Edit

+ +

Course

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/SpeakingInBitsWeb/Views/Courses/Index.cshtml b/SpeakingInBitsWeb/Views/Courses/Index.cshtml new file mode 100644 index 0000000..92dad2b --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Index.cshtml @@ -0,0 +1,37 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Courses"; +} + +
+

Courses

+ Create New Course +
+ +
+ @foreach (var item in Model) + { +
+
+
+
@item.Title
+
@item.CourseCode
+

@item.Description

+
+ Edit + Delete +
+
+
+
+ } +
+ +@if (!Model.Any()) +{ + +} diff --git a/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml b/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml index 628e80b..f24b606 100644 --- a/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml +++ b/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml @@ -1,4 +1,4 @@ - + @@ -26,6 +26,12 @@ + @if (User.Identity?.IsAuthenticated == true && User.IsInRole(Roles.Instructor)) + { + + }