From 5d9eef56ffc18228a9d268f5a543fa79263bf8ad Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Sat, 15 Aug 2026 11:33:42 +0000 Subject: [PATCH] Add TodosController with GET/POST/PATCH/DELETE endpoints --- backend/Controllers/TodosController.cs | 77 ++++++++++++++++++++++++++ backend/Dtos/CreateTodoRequest.cs | 5 ++ backend/Dtos/TodoDto.cs | 3 + backend/Dtos/UpdateTodoRequest.cs | 3 + 4 files changed, 88 insertions(+) create mode 100644 backend/Controllers/TodosController.cs create mode 100644 backend/Dtos/CreateTodoRequest.cs create mode 100644 backend/Dtos/TodoDto.cs create mode 100644 backend/Dtos/UpdateTodoRequest.cs diff --git a/backend/Controllers/TodosController.cs b/backend/Controllers/TodosController.cs new file mode 100644 index 0000000..0e08c82 --- /dev/null +++ b/backend/Controllers/TodosController.cs @@ -0,0 +1,77 @@ +using System.Security.Claims; +using Backend.Data; +using Backend.Dtos; +using Backend.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Backend.Controllers; + +[ApiController] +[Route("api/todos")] +[Authorize] +public class TodosController(AppDbContext db) : ControllerBase +{ + private Guid CurrentUserId() + { + var sub = User.FindFirstValue("sub") + ?? throw new InvalidOperationException("Authenticated request missing 'sub' claim"); + return Guid.Parse(sub); + } + + [HttpGet] + public async Task>> List() + { + var userId = CurrentUserId(); + var todos = await db.Todos + .Where(t => t.UserId == userId) + .OrderByDescending(t => t.CreatedAt) + .Select(t => new TodoDto(t.Id, t.Title, t.Completed, t.CreatedAt)) + .ToListAsync(); + return Ok(todos); + } + + [HttpPost] + public async Task> Create([FromBody] CreateTodoRequest body) + { + if (!ModelState.IsValid) return ValidationProblem(ModelState); + + var userId = CurrentUserId(); + var todo = new Todo + { + UserId = userId, + Title = body.Title, + Completed = false, + CreatedAt = DateTimeOffset.UtcNow + }; + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + var dto = new TodoDto(todo.Id, todo.Title, todo.Completed, todo.CreatedAt); + return CreatedAtAction(nameof(List), new { id = todo.Id }, dto); + } + + [HttpPatch("{id:long}")] + public async Task> Update(long id, [FromBody] UpdateTodoRequest body) + { + var userId = CurrentUserId(); + var todo = await db.Todos.FirstOrDefaultAsync(t => t.Id == id && t.UserId == userId); + if (todo is null) return NotFound(); + + if (body.Completed.HasValue) todo.Completed = body.Completed.Value; + await db.SaveChangesAsync(); + + return Ok(new TodoDto(todo.Id, todo.Title, todo.Completed, todo.CreatedAt)); + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + var userId = CurrentUserId(); + var affected = await db.Todos + .Where(t => t.Id == id && t.UserId == userId) + .ExecuteDeleteAsync(); + return affected == 0 ? NotFound() : NoContent(); + } +} diff --git a/backend/Dtos/CreateTodoRequest.cs b/backend/Dtos/CreateTodoRequest.cs new file mode 100644 index 0000000..e35f080 --- /dev/null +++ b/backend/Dtos/CreateTodoRequest.cs @@ -0,0 +1,5 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backend.Dtos; + +public record CreateTodoRequest([Required, StringLength(500, MinimumLength = 1)] string Title); diff --git a/backend/Dtos/TodoDto.cs b/backend/Dtos/TodoDto.cs new file mode 100644 index 0000000..578e258 --- /dev/null +++ b/backend/Dtos/TodoDto.cs @@ -0,0 +1,3 @@ +namespace Backend.Dtos; + +public record TodoDto(long Id, string Title, bool Completed, DateTimeOffset CreatedAt); diff --git a/backend/Dtos/UpdateTodoRequest.cs b/backend/Dtos/UpdateTodoRequest.cs new file mode 100644 index 0000000..d636911 --- /dev/null +++ b/backend/Dtos/UpdateTodoRequest.cs @@ -0,0 +1,3 @@ +namespace Backend.Dtos; + +public record UpdateTodoRequest(bool? Completed);