From 92ce193d51aab82376369a7f362043d2e2826d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Wed, 11 Sep 2024 21:41:34 +0200 Subject: [PATCH 01/17] Closes #60 Move rename request parameters into class --- backend/src/api/Controllers/TodoController.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index 0a37197..bc83dc8 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -78,14 +78,14 @@ async Task Create(string user, string name) } [HttpPut("lists/{id}")] - public async Task RenameList([FromRoute] int id, [FromBody] Request request) + public async Task RenameList(RenameRequest request) { var userId = Request.UserId(); - _logger.LogDebug("User: {user} renamed: {id} to: {list}", userId, id, request.Name); + _logger.LogDebug("User: {user} renamed: {id} to: {list}", userId, request.Id, request.Body.Name); return userId switch { null => Unauthorized(), - var user => await Rename(user, id, request.Name), + var user => await Rename(user, request.Id, request.Body.Name), }; async Task Rename(string user, int id, string name) @@ -105,3 +105,16 @@ public class Request { public required string Name { get; set; } } + +public class RenameRequest +{ + [FromRoute] + public required int Id { get; set; } + [FromBody] + public required Payload Body { get; set; } + + public class Payload + { + public required string Name { get; set; } + } +} From 9b345895618dce88b451de0ebfdfbf2a3e0db3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Wed, 11 Sep 2024 21:42:00 +0200 Subject: [PATCH 02/17] Closes #60 TIDY: Move Log enricher into infrastructure project --- backend/src/api/Program.cs | 2 +- .../src/{api => infrastructure/Logging}/LoggingExtensions.cs | 3 ++- backend/src/infrastructure/infrastructure.csproj | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) rename backend/src/{api => infrastructure/Logging}/LoggingExtensions.cs (94%) diff --git a/backend/src/api/Program.cs b/backend/src/api/Program.cs index 4e0557e..eff56b6 100644 --- a/backend/src/api/Program.cs +++ b/backend/src/api/Program.cs @@ -4,7 +4,7 @@ using Microsoft.OpenApi.Models; using Serilog; using System.Security.Cryptography; -using api; +using infrastructure.Logging; using infrastructure.Data; using infrastructure.Keycloak; using core.Ports; diff --git a/backend/src/api/LoggingExtensions.cs b/backend/src/infrastructure/Logging/LoggingExtensions.cs similarity index 94% rename from backend/src/api/LoggingExtensions.cs rename to backend/src/infrastructure/Logging/LoggingExtensions.cs index a359a02..792c2f2 100644 --- a/backend/src/api/LoggingExtensions.cs +++ b/backend/src/infrastructure/Logging/LoggingExtensions.cs @@ -1,9 +1,10 @@ +using Microsoft.AspNetCore.Http; using Serilog; using Serilog.Configuration; using Serilog.Core; using Serilog.Events; -namespace api; +namespace infrastructure.Logging; public static class LoggingExtensions { diff --git a/backend/src/infrastructure/infrastructure.csproj b/backend/src/infrastructure/infrastructure.csproj index 7d93446..92b153b 100644 --- a/backend/src/infrastructure/infrastructure.csproj +++ b/backend/src/infrastructure/infrastructure.csproj @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + From 536753df387286bbcb0ae6b2ea3a0344d61f0eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Wed, 11 Sep 2024 21:56:21 +0200 Subject: [PATCH 03/17] Closes #60 Add maping to a repsonse --- backend/src/api/Controllers/TodoController.cs | 7 ++++--- backend/src/api/Data/DTO/TodoListDTO.cs | 3 --- backend/src/api/Responses/TodoList.cs | 8 ++++++++ 3 files changed, 12 insertions(+), 6 deletions(-) delete mode 100644 backend/src/api/Data/DTO/TodoListDTO.cs create mode 100644 backend/src/api/Responses/TodoList.cs diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index bc83dc8..dbc0ddb 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -1,6 +1,7 @@ using core.Ports; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using api.Resonses; namespace api.Controllers; @@ -26,7 +27,7 @@ public IActionResult GetLists() return userId switch { null => Unauthorized(), - var user => Ok(_service.GetLists(user)), + var user => Ok(_service.GetLists(user).Select(l => l.ToResponse())), }; } @@ -36,7 +37,7 @@ public IActionResult GetList([FromRoute] int id) var userId = Request.UserId(); _logger.LogDebug("User: {user} requested his list: {id}", userId, id); if (userId is null) { return Unauthorized(); } - var list = _service.GetList(userId, id); + var list = _service.GetList(userId, id)?.ToResponse(); return list switch { null => NotFound(), @@ -73,7 +74,7 @@ async Task Create(string user, string name) { //TODO: return DTO instead of DAO var list = await _service.CreateList(user, name); - return Created(HttpContext.Request.Path.Add(new PathString($"/{list.Id}")), list); + return Created(HttpContext.Request.Path.Add(new PathString($"/{list.Id}")), list.ToResponse()); } } diff --git a/backend/src/api/Data/DTO/TodoListDTO.cs b/backend/src/api/Data/DTO/TodoListDTO.cs deleted file mode 100644 index 275e8d4..0000000 --- a/backend/src/api/Data/DTO/TodoListDTO.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace api.Data.DTO; - -public record TodoListDTO(int Id, string Name); diff --git a/backend/src/api/Responses/TodoList.cs b/backend/src/api/Responses/TodoList.cs new file mode 100644 index 0000000..9207e35 --- /dev/null +++ b/backend/src/api/Responses/TodoList.cs @@ -0,0 +1,8 @@ +namespace api.Resonses; + +public record TodoList(int Id, string Name); + +public static class MapExtensions +{ + public static TodoList ToResponse(this core.TodoList list) => new TodoList(list.Id, list.Name); +} From b16cb567876b8cc7229659b74c057d634552c3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 20 Sep 2024 19:57:35 +0200 Subject: [PATCH 04/17] Closes #60 Add todo items and tags --- .../Data/ApplicationDbContext.cs | 21 ++- backend/src/infrastructure/Data/Tag.cs | 12 ++ .../src/infrastructure/Data/TodoItemModel.cs | 20 +++ .../20240920175610_Add items.Designer.cs | 153 ++++++++++++++++++ .../Migrations/20240920175610_Add items.cs | 100 ++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 108 ++++++++++++- 6 files changed, 400 insertions(+), 14 deletions(-) create mode 100644 backend/src/infrastructure/Data/Tag.cs create mode 100644 backend/src/infrastructure/Data/TodoItemModel.cs create mode 100644 backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs create mode 100644 backend/src/infrastructure/Migrations/20240920175610_Add items.cs diff --git a/backend/src/infrastructure/Data/ApplicationDbContext.cs b/backend/src/infrastructure/Data/ApplicationDbContext.cs index df8dfd3..f091328 100644 --- a/backend/src/infrastructure/Data/ApplicationDbContext.cs +++ b/backend/src/infrastructure/Data/ApplicationDbContext.cs @@ -1,11 +1,10 @@ -using Microsoft.EntityFrameworkCore; - -namespace infrastructure.Data; - -public class ApplicationDbContext : DbContext -{ - public ApplicationDbContext(DbContextOptions options) : base(options) - { - } - public DbSet TodoLists { get; set; } -} +using Microsoft.EntityFrameworkCore; + +namespace infrastructure.Data; + +public class ApplicationDbContext(DbContextOptions options) : DbContext(options) +{ + public required DbSet TodoLists { get; set; } + public required DbSet Todos { get; set; } + public required DbSet Tags { get; set; } +} diff --git a/backend/src/infrastructure/Data/Tag.cs b/backend/src/infrastructure/Data/Tag.cs new file mode 100644 index 0000000..f48170b --- /dev/null +++ b/backend/src/infrastructure/Data/Tag.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace infrastructure.Data; + +public class Tag { + [Key] + public int Id { get; set; } + [MaxLength(512)] + public required string Name { get; set; } + public required Guid Owner { get; set; } + public required IEnumerable Items {get; set;} +} \ No newline at end of file diff --git a/backend/src/infrastructure/Data/TodoItemModel.cs b/backend/src/infrastructure/Data/TodoItemModel.cs new file mode 100644 index 0000000..f93fa9e --- /dev/null +++ b/backend/src/infrastructure/Data/TodoItemModel.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace infrastructure.Data; + +public class TodoItemModel +{ + [Key] + public int Id {get; set; } + [MaxLength(512)] + public required string Name { get; set; } + public required Guid Owner { get; set; } + public bool Compleated {get; set;} + public DateTime Deadline {get; set;} + [MaxLength(4096)] + public string Notes {get; set;} = null!; + public required IEnumerable Tags {get; set;} + public required IEnumerable Subtasks {get; set;} + public int? MainTaskId {get; set;} + public TodoItemModel? MainTask {get; set;} +} \ No newline at end of file diff --git a/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs b/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs new file mode 100644 index 0000000..05b521b --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs @@ -0,0 +1,153 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using infrastructure.Data; + +#nullable disable + +namespace infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240920175610_Add items")] + partial class Additems + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TagTodoItemModel", b => + { + b.Property("ItemsId") + .HasColumnType("integer"); + + b.Property("TagsId") + .HasColumnType("integer"); + + b.HasKey("ItemsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("TagTodoItemModel"); + }); + + modelBuilder.Entity("infrastructure.Data.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Compleated") + .HasColumnType("boolean"); + + b.Property("Deadline") + .HasColumnType("timestamp with time zone"); + + b.Property("MainTaskId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MainTaskId"); + + b.ToTable("Todos"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("todo_lists"); + }); + + modelBuilder.Entity("TagTodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", "MainTask") + .WithMany("Subtasks") + .HasForeignKey("MainTaskId"); + + b.Navigation("MainTask"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Navigation("Subtasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/infrastructure/Migrations/20240920175610_Add items.cs b/backend/src/infrastructure/Migrations/20240920175610_Add items.cs new file mode 100644 index 0000000..7278e61 --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240920175610_Add items.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace infrastructure.Migrations +{ + /// + public partial class Additems : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Owner = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Todos", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Owner = table.Column(type: "uuid", nullable: false), + Compleated = table.Column(type: "boolean", nullable: false), + Deadline = table.Column(type: "timestamp with time zone", nullable: false), + Notes = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + MainTaskId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Todos", x => x.Id); + table.ForeignKey( + name: "FK_Todos_Todos_MainTaskId", + column: x => x.MainTaskId, + principalTable: "Todos", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "TagTodoItemModel", + columns: table => new + { + ItemsId = table.Column(type: "integer", nullable: false), + TagsId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TagTodoItemModel", x => new { x.ItemsId, x.TagsId }); + table.ForeignKey( + name: "FK_TagTodoItemModel_Tags_TagsId", + column: x => x.TagsId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TagTodoItemModel_Todos_ItemsId", + column: x => x.ItemsId, + principalTable: "Todos", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TagTodoItemModel_TagsId", + table: "TagTodoItemModel", + column: "TagsId"); + + migrationBuilder.CreateIndex( + name: "IX_Todos_MainTaskId", + table: "Todos", + column: "MainTaskId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TagTodoItemModel"); + + migrationBuilder.DropTable( + name: "Tags"); + + migrationBuilder.DropTable( + name: "Todos"); + } + } +} diff --git a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 42e6371..399b238 100644 --- a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -17,12 +17,85 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("api.Data.DAO.TodoListModel", b => + modelBuilder.Entity("TagTodoItemModel", b => + { + b.Property("ItemsId") + .HasColumnType("integer"); + + b.Property("TagsId") + .HasColumnType("integer"); + + b.HasKey("ItemsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("TagTodoItemModel"); + }); + + modelBuilder.Entity("infrastructure.Data.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Compleated") + .HasColumnType("boolean"); + + b.Property("Deadline") + .HasColumnType("timestamp with time zone"); + + b.Property("MainTaskId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MainTaskId"); + + b.ToTable("Todos"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -42,6 +115,35 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("todo_lists"); }); + + modelBuilder.Entity("TagTodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", "MainTask") + .WithMany("Subtasks") + .HasForeignKey("MainTaskId"); + + b.Navigation("MainTask"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Navigation("Subtasks"); + }); #pragma warning restore 612, 618 } } From a2854cc0aa9ff9ae65bd2b7f20237b6b09d59ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Mon, 23 Sep 2024 12:16:05 +0200 Subject: [PATCH 05/17] Closes #60 Base implementation of todo items operations --- .../bruno/KSummarized/ToDo/Items/Create.bru | 53 ++++++ .../bruno/KSummarized/ToDo/Items/Delete.bru | 46 +++++ backend/bruno/KSummarized/ToDo/Items/Get.bru | 50 ++++++ backend/bruno/KSummarized/ToDo/Items/List.bru | 46 +++++ .../bruno/KSummarized/ToDo/Items/Update.bru | 46 +++++ backend/ksummarized.sln | 7 + backend/src/api/Controllers/TodoController.cs | 79 +++++++- backend/src/core/Ports/ITodoService.cs | 5 + backend/src/core/Tag.cs | 5 + backend/src/core/TodoItem.cs | 12 ++ backend/src/infrastructure/Data/Tag.cs | 9 +- .../src/infrastructure/Data/TodoItemModel.cs | 18 +- .../src/infrastructure/Data/TodoService.cs | 169 ++++++++++++++++++ .../src/infrastructure/LoggingExtensions.cs | 1 + 14 files changed, 525 insertions(+), 21 deletions(-) create mode 100644 backend/bruno/KSummarized/ToDo/Items/Create.bru create mode 100644 backend/bruno/KSummarized/ToDo/Items/Delete.bru create mode 100644 backend/bruno/KSummarized/ToDo/Items/Get.bru create mode 100644 backend/bruno/KSummarized/ToDo/Items/List.bru create mode 100644 backend/bruno/KSummarized/ToDo/Items/Update.bru create mode 100644 backend/src/core/Tag.cs create mode 100644 backend/src/core/TodoItem.cs create mode 100644 backend/src/infrastructure/LoggingExtensions.cs diff --git a/backend/bruno/KSummarized/ToDo/Items/Create.bru b/backend/bruno/KSummarized/ToDo/Items/Create.bru new file mode 100644 index 0000000..7e50b18 --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Create.bru @@ -0,0 +1,53 @@ +meta { + name: Create + type: http + seq: 1 +} + +post { + url: https://{{api_base_url}}/api/todo/items + body: json + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "id": 1, + "name": "Demo", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there", + "tags": [{ + "id":1, + "name": "star wars" + }], + "subtasks": [ + { + "id": 2, + "name": "Demo", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there2", + "tags": [], + "subtasks": [] + }, + { + "id": 3, + "name": "Demo3", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there 3", + "tags": [], + "subtasks": [] + } + ] + } +} + +vars:post-response { + itemId: res.body.id +} diff --git a/backend/bruno/KSummarized/ToDo/Items/Delete.bru b/backend/bruno/KSummarized/ToDo/Items/Delete.bru new file mode 100644 index 0000000..eebea05 --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Delete.bru @@ -0,0 +1,46 @@ +meta { + name: Delete + type: http + seq: 5 +} + +delete { + url: https://{{api_base_url}}/api/todo/items/{{itemId}} + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "id": 1, + "name": "Demo-Updated", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there", + "tags": [], + "subtasks": [ + { + "id": 2, + "name": "Demo2", + "compleated": true, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there2", + "tags": [], + "subtasks": [] + }, + { + "id": 3, + "name": "Demo22", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there 3", + "tags": [], + "subtasks": [] + } + ] + } +} diff --git a/backend/bruno/KSummarized/ToDo/Items/Get.bru b/backend/bruno/KSummarized/ToDo/Items/Get.bru new file mode 100644 index 0000000..7576768 --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Get.bru @@ -0,0 +1,50 @@ +meta { + name: Get + type: http + seq: 3 +} + +get { + url: https://{{api_base_url}}/api/todo/items/{{itemId}} + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "id": 1, + "name": "Demo", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there", + "tags": [], + "subtasks": [ + { + "id": 2, + "name": "Demo", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there2", + "tags": [], + "subtasks": [] + }, + { + "id": 3, + "name": "Demo3", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there 3", + "tags": [], + "subtasks": [] + } + ] + } +} + +vars:post-response { + listId: res.body.id +} diff --git a/backend/bruno/KSummarized/ToDo/Items/List.bru b/backend/bruno/KSummarized/ToDo/Items/List.bru new file mode 100644 index 0000000..c11a33e --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/List.bru @@ -0,0 +1,46 @@ +meta { + name: List + type: http + seq: 4 +} + +get { + url: https://{{api_base_url}}/api/todo/items + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "id": 1, + "name": "Demo", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there", + "tags": [], + "subtasks": [ + { + "id": 2, + "name": "Demo", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there2", + "tags": [], + "subtasks": [] + }, + { + "id": 3, + "name": "Demo3", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there 3", + "tags": [], + "subtasks": [] + } + ] + } +} diff --git a/backend/bruno/KSummarized/ToDo/Items/Update.bru b/backend/bruno/KSummarized/ToDo/Items/Update.bru new file mode 100644 index 0000000..e60d720 --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Update.bru @@ -0,0 +1,46 @@ +meta { + name: Update + type: http + seq: 2 +} + +put { + url: https://{{api_base_url}}/api/todo/items/{{itemId}} + body: json + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "id": {{itemId}}, + "name": "Demo-Updated", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there", + "tags": [], + "subtasks": [ + { + "id": 2, + "name": "Demo2", + "compleated": true, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there2", + "tags": [], + "subtasks": [] + }, + { + "id": 3, + "name": "Demo22", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there 3", + "tags": [], + "subtasks": [] + } + ] + } +} diff --git a/backend/ksummarized.sln b/backend/ksummarized.sln index 20d82c5..1b0a635 100644 --- a/backend/ksummarized.sln +++ b/backend/ksummarized.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unit", "test\unit\unit.cspr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "infrastructure", "src\infrastructure\infrastructure.csproj", "{9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "core", "src\core\core.csproj", "{75F10B5B-9DD4-4B97-B571-DC9F517B839F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,10 +36,15 @@ Global {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}.Release|Any CPU.Build.0 = Release|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {949171D5-4018-4C40-AB2B-687B39F20974} = {DE2804D2-3D3D-4A41-91C7-4FEEF71A4434} {B930D2FA-7FC6-4772-89EA-6B925EDD2EBC} = {4CA15B69-5E75-48E2-BE26-EA818B675CFC} {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB} = {DE2804D2-3D3D-4A41-91C7-4FEEF71A4434} + {75F10B5B-9DD4-4B97-B571-DC9F517B839F} = {DE2804D2-3D3D-4A41-91C7-4FEEF71A4434} EndGlobalSection EndGlobal diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index dbc0ddb..0986aec 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -2,22 +2,17 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using api.Resonses; +using core; namespace api.Controllers; [Authorize] [Route("/api/todo")] [ApiController] -public class TodoController : ControllerBase +public class TodoController(ITodoService service, ILogger logger) : ControllerBase { - private readonly ITodoService _service; - private readonly ILogger _logger; - - public TodoController(ITodoService service, ILogger logger) - { - _service = service; - _logger = logger; - } + private readonly ITodoService _service = service; + private readonly ILogger _logger = logger; [HttpGet("lists")] public IActionResult GetLists() @@ -100,6 +95,72 @@ async Task Rename(string user, int id, string name) return BadRequest(); } } + + [HttpPost("items")] + public async Task CreateItem([FromBody] TodoItem request) + { + var userId = Request.UserId(); + _logger.LogDebug("User: {user} created: {item}", userId, request.Name); + return userId switch + { + null => Unauthorized(), + var user => await Create(user, request), + }; + + async Task Create(string user, TodoItem item) + { + var newItem = await _service.CreateItem(user, item); + return Created(HttpContext.Request.Path.Add(new PathString($"/{newItem.Id}")), newItem); + } + } + + [HttpGet("items")] + public IActionResult ListItems() + { + var userId = Request.UserId(); + _logger.LogDebug("User: {user} requested his items", userId); + return userId switch + { + null => Unauthorized(), + var user => Ok(_service.ListItems(user)), + }; + } + + [HttpGet("items/{id}")] + public async Task GetItem([FromRoute] int id) + { + var userId = Request.UserId(); + _logger.LogDebug("User: {user} requested his item: {id}", userId, id); + return userId switch + { + null => Unauthorized(), + var user => Ok(await _service.GetItem(user, id)), + }; + } + + [HttpDelete("items/{id}")] + public async Task DeleteItem([FromRoute] int id) + { + var userId = Request.UserId(); + _logger.LogDebug("User: {user} deleted his item: {id}", userId, id); + return userId switch + { + null => Unauthorized(), + var user => Ok(await _service.DeleteItem(user, id)), + }; + } + + [HttpPut("items/{id}")] + public async Task UpdateItem([FromRoute] int id, [FromBody] TodoItem request) + { + var userId = Request.UserId(); + _logger.LogDebug("User: {user} updated his item: {id}", userId, id); + return userId switch + { + null => Unauthorized(), + var user => Ok(await _service.UpdateItem(user, request)), + }; + } } public class Request diff --git a/backend/src/core/Ports/ITodoService.cs b/backend/src/core/Ports/ITodoService.cs index 21ef07a..fd2f37e 100644 --- a/backend/src/core/Ports/ITodoService.cs +++ b/backend/src/core/Ports/ITodoService.cs @@ -7,4 +7,9 @@ public interface ITodoService TodoList? GetList(string userId, int id); bool DeleteList(string userId, int id); Task RenameList(string user, int id, string name); + Task CreateItem(string user, TodoItem item); + Task GetItem(string user, int id); + IEnumerable ListItems(string user); + Task DeleteItem(string user, int id); + Task UpdateItem(string user, TodoItem item); } diff --git a/backend/src/core/Tag.cs b/backend/src/core/Tag.cs new file mode 100644 index 0000000..c25435f --- /dev/null +++ b/backend/src/core/Tag.cs @@ -0,0 +1,5 @@ +namespace core; +public class Tag{ + public int Id {get; set;} + public required string Name {get; set;} +} \ No newline at end of file diff --git a/backend/src/core/TodoItem.cs b/backend/src/core/TodoItem.cs new file mode 100644 index 0000000..dbc2f2e --- /dev/null +++ b/backend/src/core/TodoItem.cs @@ -0,0 +1,12 @@ +namespace core; + +public record TodoItem +{ + public int? Id { get; set; } + public required string Name { get; set; } + public bool Compleated {get; set;} + public DateTime Deadline {get; set;} + public string Notes { get; set; } = null!; + public required IEnumerable Tags { get; set; } + public required IEnumerable Subtasks { get; set; } +} \ No newline at end of file diff --git a/backend/src/infrastructure/Data/Tag.cs b/backend/src/infrastructure/Data/Tag.cs index f48170b..78bd2fa 100644 --- a/backend/src/infrastructure/Data/Tag.cs +++ b/backend/src/infrastructure/Data/Tag.cs @@ -1,12 +1,15 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace infrastructure.Data; -public class Tag { +[Table("Tags")] +public class Tag +{ [Key] public int Id { get; set; } [MaxLength(512)] public required string Name { get; set; } public required Guid Owner { get; set; } - public required IEnumerable Items {get; set;} -} \ No newline at end of file + public ICollection Items { get; set; } = null!; +} diff --git a/backend/src/infrastructure/Data/TodoItemModel.cs b/backend/src/infrastructure/Data/TodoItemModel.cs index f93fa9e..d3d1989 100644 --- a/backend/src/infrastructure/Data/TodoItemModel.cs +++ b/backend/src/infrastructure/Data/TodoItemModel.cs @@ -5,16 +5,16 @@ namespace infrastructure.Data; public class TodoItemModel { [Key] - public int Id {get; set; } + public int Id { get; set; } [MaxLength(512)] public required string Name { get; set; } public required Guid Owner { get; set; } - public bool Compleated {get; set;} - public DateTime Deadline {get; set;} + public bool Compleated { get; set; } + public DateTime Deadline { get; set; } [MaxLength(4096)] - public string Notes {get; set;} = null!; - public required IEnumerable Tags {get; set;} - public required IEnumerable Subtasks {get; set;} - public int? MainTaskId {get; set;} - public TodoItemModel? MainTask {get; set;} -} \ No newline at end of file + public string Notes { get; set; } = null!; + public required ICollection Tags { get; set; } + public required ICollection Subtasks { get; set; } + public int? MainTaskId { get; set; } + public TodoItemModel? MainTask { get; set; } +} diff --git a/backend/src/infrastructure/Data/TodoService.cs b/backend/src/infrastructure/Data/TodoService.cs index 3d2dcc0..d89a24a 100644 --- a/backend/src/infrastructure/Data/TodoService.cs +++ b/backend/src/infrastructure/Data/TodoService.cs @@ -1,6 +1,9 @@ using Microsoft.EntityFrameworkCore; using core.Ports; using core; +using Serilog; +using System.Text.Json; +using System.Text.Json.Serialization; namespace infrastructure.Data; @@ -59,4 +62,170 @@ public async Task RenameList(string userId, int id, string name) await _context.SaveChangesAsync(); return true; } + + public async Task CreateItem(string user, TodoItem item) + { + var newItem = new TodoItemModel() + { + Name = item.Name, + Owner = Guid.Parse(user), + Compleated = false, + Deadline = item.Deadline, + Notes = item.Notes, + Subtasks = [], + Tags = [] + }; + foreach (var st in item.Subtasks) + { + var newSubtask = new TodoItemModel() + { + Name = st.Name, + Owner = Guid.Parse(user), + Compleated = false, + Deadline = st.Deadline, + Notes = st.Notes, + Tags = [], + Subtasks = [] + }; + newItem.Subtasks.Add(newSubtask); + } + foreach (var tag in item.Tags) + { + var t = _context.Tags.FirstOrDefault(t => t.Id == tag.Id && t.Owner.Equals(Guid.Parse(user))); + if (t is null) + { + newItem.Tags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = Guid.Parse(user) }); + } + else + { + t.Name = tag.Name; + } + } + await _context.Todos.AddAsync(newItem); + await _context.SaveChangesAsync(); + return item with { Id = newItem.Id }; + } + + public async Task GetItem(string user, int id) + { + var item = await _context.Todos + .AsNoTracking() + .Include(i => i.Subtasks) + .Include(i => i.Tags) + .AsSplitQuery() + .SingleOrDefaultAsync(i => i.Owner.Equals(Guid.Parse(user)) && i.Id == id); + if (item is null) { return null; } + + return MapTodoItem(item); + } + + public IEnumerable ListItems(string user) + { + return _context.Todos + .AsNoTracking() + .Include(i => i.Subtasks) + .Include(i => i.Tags) + .Where(i => i.Owner.Equals(Guid.Parse(user)) && i.MainTaskId == null) + .AsSplitQuery() + .Select(i => MapTodoItem(i)) + .AsEnumerable(); + } + + public async Task DeleteItem(string user, int id) + { + var item = _context.Todos + .Include(i => i.Subtasks) + .SingleOrDefault(i => i.Owner.Equals(Guid.Parse(user)) && i.Id == id); + if (item is null) { return false; } + if (item.Subtasks.Any()) + { + _context.Todos.RemoveRange(item.Subtasks); + } + _context.Todos.Remove(item); + await _context.SaveChangesAsync(); + return true; + } + + public async Task UpdateItem(string user, TodoItem item) + { + var existingItem = _context.Todos + .Include(i => i.Subtasks) + .Include(i => i.Tags) + .AsSplitQuery() + .SingleOrDefault(i => i.Owner.Equals(Guid.Parse(user)) && i.Id == item.Id); + if (existingItem is null) { return false; } + existingItem.Name = item.Name; + existingItem.Deadline = item.Deadline; + existingItem.Notes = item.Notes; + existingItem.Compleated = item.Compleated; + foreach (var st in item.Subtasks) + { + var existingSubtask = existingItem.Subtasks.FirstOrDefault(st => st.Id == st.Id); + if (existingSubtask is null) + { + var newSubtask = new TodoItemModel() + { + Name = st.Name, + Owner = Guid.Parse(user), + Compleated = st.Compleated, + Deadline = st.Deadline, + Notes = st.Notes, + Tags = [], + Subtasks = [] + }; + existingItem.Subtasks.Add(newSubtask); + } + else + { + existingSubtask.Name = st.Name; + existingSubtask.Deadline = st.Deadline; + existingSubtask.Notes = st.Notes; + existingSubtask.Compleated = st.Compleated; + } + } + var existingTags = existingItem.Tags.ToList(); + foreach (var tag in item.Tags) + { + var t = existingTags.FirstOrDefault(t => t.Id == tag.Id); + if (t is null) + { + existingTags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = Guid.Parse(user) }); + } + else + { + t.Name = tag.Name; + } + } + existingItem.Tags = existingTags; + + await _context.SaveChangesAsync(); + return true; + } + + private static TodoItem MapTodoItem(infrastructure.Data.TodoItemModel item) + { + Log.Information("Mapped {item}", JsonSerializer.Serialize(item, new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve })); + return new TodoItem() + { + Id = item.Id, + Name = item.Name, + Deadline = item.Deadline, + Notes = item.Notes, + Subtasks = item.Subtasks?.Select(st => MapSubtask(st)).ToList() ?? [], + Tags = item.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [] + }; + } + + private static TodoItem MapSubtask(infrastructure.Data.TodoItemModel subtask) + { + return new TodoItem() + { + Id = subtask.Id, + Name = subtask.Name, + Deadline = subtask.Deadline, + Notes = subtask.Notes, + Subtasks = [], + Tags = subtask.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [] + }; + } } diff --git a/backend/src/infrastructure/LoggingExtensions.cs b/backend/src/infrastructure/LoggingExtensions.cs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/src/infrastructure/LoggingExtensions.cs @@ -0,0 +1 @@ + From ed90653ec2c88b2c0a6216e01ffd531ef29d6713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 20 Sep 2024 19:59:39 +0200 Subject: [PATCH 06/17] Closes #60 Add information about working with migrations in README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 8507806..f09d921 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,8 @@ docker compose --profile without-hot-reload up --build --watch The `--watch` parameter starts containers with the `hot-reload` feature. This enables the auto-reload functionality, meaning that the container will be automatically reloaded when the code for either the frontend or backend changes. > The `hot-reload` feature for backend applications uses `dotnet watch`, which only detects changes to existing files. It will not restart the container if new files are added (dotnet watch [issue](https://github.com/dotnet/aspnetcore/issues/8321)). + +### DB Migrations +When working with migrations remember to add parameter for project and startup project. +For example when generating a new migration while in backend directory: +`dotnet ef migrations add -p ./src/infrastructure -s ./src/api "Example"` From c5ddb62a0d13f4fc6c2c1609afa7488867d81650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 17:50:17 +0200 Subject: [PATCH 07/17] Closes #60 Imporve API response codes --- backend/src/api/Controllers/TodoController.cs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index 0986aec..be20025 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -104,12 +104,16 @@ public async Task CreateItem([FromBody] TodoItem request) return userId switch { null => Unauthorized(), - var user => await Create(user, request), + var user => await Handle(user, request), }; - async Task Create(string user, TodoItem item) + async Task Handle(string user, TodoItem item) { var newItem = await _service.CreateItem(user, item); + if (newItem is null) + { + return BadRequest(); + } return Created(HttpContext.Request.Path.Add(new PathString($"/{newItem.Id}")), newItem); } } @@ -134,8 +138,18 @@ public async Task GetItem([FromRoute] int id) return userId switch { null => Unauthorized(), - var user => Ok(await _service.GetItem(user, id)), + var user => await Handle(user, id), }; + + async Task Handle(string user, int id) + { + var item = await _service.GetItem(user, id); + if (item is null) + { + return NotFound(); + } + return Ok(item); + } } [HttpDelete("items/{id}")] @@ -146,8 +160,18 @@ public async Task DeleteItem([FromRoute] int id) return userId switch { null => Unauthorized(), - var user => Ok(await _service.DeleteItem(user, id)), + var user => await Handle(user, id), }; + + async Task Handle(string user, int id) + { + var success = await _service.DeleteItem(user, id); + if (success) + { + return Ok(); + } + return BadRequest(); + } } [HttpPut("items/{id}")] @@ -158,8 +182,18 @@ public async Task UpdateItem([FromRoute] int id, [FromBody] TodoI return userId switch { null => Unauthorized(), - var user => Ok(await _service.UpdateItem(user, request)), + var user => await Handle(user, id), }; + + async Task Handle(string user, int id) + { + var success = await _service.UpdateItem(user, request); + if (success) + { + return Ok(); + } + return BadRequest(); + } } } From 390d44d20b4e32bc1e60abebea5e6361864071a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 17:50:45 +0200 Subject: [PATCH 08/17] Closes #60 Introduce and document a Makefile for ease of application launch --- Makefile | 8 ++++++++ README.md | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fbcc81a --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +dev: + docker compose --profile hot-reload up --build --watch + +no-reload: + docker compose --profile without-hot-reload up --build + +.PHONY: dev no-reload +.DEFAULT_GOAL := dev diff --git a/README.md b/README.md index f09d921..e668445 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,16 @@ Directory `scripts` contains some helpful scripts which automate some parts of w ## Development -The application is started using the following command: +The application is started using the following command (with hot-reload feature): +```bash +make +``` +or with the hot-reload feature: +```bash +make no-reload +``` +If you want to run the application manually you can use the following command: ```bash docker compose --profile hot-reload up --build --watch ``` From 9cacf1e4b9f43359a772eba0c7a05c3684e7ba92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 19:31:23 +0200 Subject: [PATCH 09/17] Closes #60 Introduce UserIdFilter to minimize code repetition in controllers --- backend/src/api/Controllers/TodoController.cs | 3 +++ backend/src/api/Filters/UserIdFilter.cs | 26 +++++++++++++++++++ backend/src/api/Program.cs | 7 ++--- backend/src/api/RequestExtensions.cs | 2 +- 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 backend/src/api/Filters/UserIdFilter.cs diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index be20025..c1f7189 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -2,17 +2,20 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using api.Resonses; +using api.Filters; using core; namespace api.Controllers; [Authorize] +[UserIdFilter] [Route("/api/todo")] [ApiController] public class TodoController(ITodoService service, ILogger logger) : ControllerBase { private readonly ITodoService _service = service; private readonly ILogger _logger = logger; + private Guid UserId => (Guid)HttpContext.Items["UserId"]!; [HttpGet("lists")] public IActionResult GetLists() diff --git a/backend/src/api/Filters/UserIdFilter.cs b/backend/src/api/Filters/UserIdFilter.cs new file mode 100644 index 0000000..d7b8bbb --- /dev/null +++ b/backend/src/api/Filters/UserIdFilter.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; + +namespace api.Filters; + +public class UserIdFilter : Attribute, IAuthorizationFilter +{ + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext?.User?.Identity?.IsAuthenticated ?? false) + { + context.Result = new UnauthorizedResult(); + } + var userId = context.HttpContext?.Request.UserId(); + if (userId == null) + { + context.Result = new UnauthorizedResult(); + } + var valid = Guid.TryParse(userId, out Guid id); + if (!valid) + { + context.Result = new UnauthorizedResult(); + } + context.HttpContext!.Items["UserId"] = id; + } +} diff --git a/backend/src/api/Program.cs b/backend/src/api/Program.cs index eff56b6..1df442b 100644 --- a/backend/src/api/Program.cs +++ b/backend/src/api/Program.cs @@ -4,10 +4,11 @@ using Microsoft.OpenApi.Models; using Serilog; using System.Security.Cryptography; -using infrastructure.Logging; +using api.Filters; +using core.Ports; using infrastructure.Data; using infrastructure.Keycloak; -using core.Ports; +using infrastructure.Logging; const string logFormat = "[{Timestamp:HH:mm:ss} {Level:u3}] {CorelationId} | {Message:lj}{NewLine}{Exception}"; Log.Logger = new LoggerConfiguration().Enrich.WithCorrelationId() @@ -57,7 +58,7 @@ options.TokenValidationParameters = tokenValidationParameters; }); - builder.Services.AddControllers(); + builder.Services.AddControllers(o => o.Filters.Add(typeof(UserIdFilter))); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "api", Version = "v1" }); diff --git a/backend/src/api/RequestExtensions.cs b/backend/src/api/RequestExtensions.cs index 2d45f01..5bb893b 100644 --- a/backend/src/api/RequestExtensions.cs +++ b/backend/src/api/RequestExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.JsonWebTokens; namespace api; From 84564a3ce3ec2b6cb2cba15f26dd578e73a47511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 20:00:31 +0200 Subject: [PATCH 10/17] Closes #60 Refactor auth check to not be in controller endpoints --- backend/src/api/Controllers/TodoController.cs | 148 +++++------------- backend/src/core/Ports/ITodoService.cs | 20 +-- .../src/infrastructure/Data/TodoService.cs | 50 +++--- 3 files changed, 71 insertions(+), 147 deletions(-) diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index c1f7189..0aab511 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -20,22 +20,15 @@ public class TodoController(ITodoService service, ILogger logger [HttpGet("lists")] public IActionResult GetLists() { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} requested his lists", userId); - return userId switch - { - null => Unauthorized(), - var user => Ok(_service.GetLists(user).Select(l => l.ToResponse())), - }; + _logger.LogDebug("User: {user} requested his lists", UserId); + return Ok(_service.GetLists(UserId).Select(l => l.ToResponse())); } [HttpGet("lists/{id}")] public IActionResult GetList([FromRoute] int id) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} requested his list: {id}", userId, id); - if (userId is null) { return Unauthorized(); } - var list = _service.GetList(userId, id)?.ToResponse(); + _logger.LogDebug("User: {user} requested his list: {id}", UserId, id); + var list = _service.GetList(UserId, id)?.ToResponse(); return list switch { null => NotFound(), @@ -46,10 +39,8 @@ public IActionResult GetList([FromRoute] int id) [HttpDelete("lists/{id}")] public IActionResult DeleteList([FromRoute] int id) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} deleted his list: {id}", userId, id); - if (userId is null) { return Unauthorized(); } - var success = _service.DeleteList(userId, id); + _logger.LogDebug("User: {user} deleted his list: {id}", UserId, id); + var success = _service.DeleteList(UserId, id); if (success) { return Ok(); @@ -60,143 +51,76 @@ public IActionResult DeleteList([FromRoute] int id) [HttpPost("lists")] public async Task CreateLists([FromBody] Request request) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} created: {list}", userId, request.Name); - return userId switch - { - null => Unauthorized(), - var user => await Create(user, request.Name), - }; - - async Task Create(string user, string name) - { - //TODO: return DTO instead of DAO - var list = await _service.CreateList(user, name); - return Created(HttpContext.Request.Path.Add(new PathString($"/{list.Id}")), list.ToResponse()); - } + _logger.LogDebug("User: {user} created: {list}", UserId, request.Name); + var list = await _service.CreateList(UserId, request.Name); + return Created(HttpContext.Request.Path.Add(new PathString($"/{list.Id}")), list.ToResponse()); } [HttpPut("lists/{id}")] public async Task RenameList(RenameRequest request) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} renamed: {id} to: {list}", userId, request.Id, request.Body.Name); - return userId switch - { - null => Unauthorized(), - var user => await Rename(user, request.Id, request.Body.Name), - }; - - async Task Rename(string user, int id, string name) + _logger.LogDebug("User: {user} renamed: {id} to: {list}", UserId, request.Id, request.Body.Name); + var list = await _service.RenameList(UserId, request.Id, request.Body.Name); + if (list) { - //TODO: return DTO instead of DAO - var list = await _service.RenameList(user, id, name); - if (list) - { - return Ok(); - } - return BadRequest(); + return Ok(); } + return BadRequest(); } [HttpPost("items")] public async Task CreateItem([FromBody] TodoItem request) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} created: {item}", userId, request.Name); - return userId switch + _logger.LogDebug("User: {user} created: {item}", UserId, request.Name); + var newItem = await _service.CreateItem(UserId, request); + if (newItem is null) { - null => Unauthorized(), - var user => await Handle(user, request), - }; - - async Task Handle(string user, TodoItem item) - { - var newItem = await _service.CreateItem(user, item); - if (newItem is null) - { - return BadRequest(); - } - return Created(HttpContext.Request.Path.Add(new PathString($"/{newItem.Id}")), newItem); + return BadRequest(); } + return Created(HttpContext.Request.Path.Add(new PathString($"/{newItem.Id}")), newItem); } [HttpGet("items")] public IActionResult ListItems() { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} requested his items", userId); - return userId switch - { - null => Unauthorized(), - var user => Ok(_service.ListItems(user)), - }; + _logger.LogDebug("User: {user} requested his items", UserId); + return Ok(_service.ListItems(UserId)); } [HttpGet("items/{id}")] public async Task GetItem([FromRoute] int id) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} requested his item: {id}", userId, id); - return userId switch + _logger.LogDebug("User: {user} requested his item: {id}", UserId, id); + var item = await _service.GetItem(UserId, id); + if (item is null) { - null => Unauthorized(), - var user => await Handle(user, id), - }; - - async Task Handle(string user, int id) - { - var item = await _service.GetItem(user, id); - if (item is null) - { - return NotFound(); - } - return Ok(item); + return NotFound(); } + return Ok(item); } [HttpDelete("items/{id}")] public async Task DeleteItem([FromRoute] int id) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} deleted his item: {id}", userId, id); - return userId switch - { - null => Unauthorized(), - var user => await Handle(user, id), - }; - - async Task Handle(string user, int id) + _logger.LogDebug("User: {user} deleted his item: {id}", UserId, id); + var success = await _service.DeleteItem(UserId, id); + if (success) { - var success = await _service.DeleteItem(user, id); - if (success) - { - return Ok(); - } - return BadRequest(); + return Ok(); } + return BadRequest(); } [HttpPut("items/{id}")] public async Task UpdateItem([FromRoute] int id, [FromBody] TodoItem request) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} updated his item: {id}", userId, id); - return userId switch - { - null => Unauthorized(), - var user => await Handle(user, id), - }; - - async Task Handle(string user, int id) + _logger.LogDebug("User: {user} updated his item: {id}", UserId, id); + var success = await _service.UpdateItem(UserId, request); + if (success) { - var success = await _service.UpdateItem(user, request); - if (success) - { - return Ok(); - } - return BadRequest(); + return Ok(); } + return BadRequest(); } } diff --git a/backend/src/core/Ports/ITodoService.cs b/backend/src/core/Ports/ITodoService.cs index fd2f37e..65d6704 100644 --- a/backend/src/core/Ports/ITodoService.cs +++ b/backend/src/core/Ports/ITodoService.cs @@ -2,14 +2,14 @@ namespace core.Ports; public interface ITodoService { - Task CreateList(string user, string name); - IEnumerable GetLists(string userId); - TodoList? GetList(string userId, int id); - bool DeleteList(string userId, int id); - Task RenameList(string user, int id, string name); - Task CreateItem(string user, TodoItem item); - Task GetItem(string user, int id); - IEnumerable ListItems(string user); - Task DeleteItem(string user, int id); - Task UpdateItem(string user, TodoItem item); + Task CreateList(Guid user, string name); + IEnumerable GetLists(Guid user); + TodoList? GetList(Guid user, int id); + bool DeleteList(Guid user, int id); + Task RenameList(Guid user, int id, string name); + Task CreateItem(Guid user, TodoItem item); + Task GetItem(Guid user, int id); + IEnumerable ListItems(Guid user); + Task DeleteItem(Guid user, int id); + Task UpdateItem(Guid user, TodoItem item); } diff --git a/backend/src/infrastructure/Data/TodoService.cs b/backend/src/infrastructure/Data/TodoService.cs index d89a24a..6e08cf9 100644 --- a/backend/src/infrastructure/Data/TodoService.cs +++ b/backend/src/infrastructure/Data/TodoService.cs @@ -17,45 +17,45 @@ public TodoService(ApplicationDbContext context) _context = context; } - public IEnumerable GetLists(string userId) + public IEnumerable GetLists(Guid user) { return _context.TodoLists.AsNoTracking() - .Where(list => list.Owner.Equals(Guid.Parse(userId))) + .Where(list => list.Owner.Equals(user)) .Select(list => new TodoList { Id = list.Id, Name = list.Name, Owner = list.Owner }) .AsEnumerable(); } - public TodoList? GetList(string userId, int id) + public TodoList? GetList(Guid user, int id) { var list = _context.TodoLists.AsNoTracking() - .SingleOrDefault(l => l.Owner.Equals(Guid.Parse(userId)) && l.Id == id); + .SingleOrDefault(l => l.Owner.Equals(user) && l.Id == id); //TODO: Consider creating a mapper instead of this manual new if (list is not null) { return new() { Id = list.Id, Name = list.Name, Owner = list.Owner }; } return null; } - public async Task CreateList(string user, string name) + public async Task CreateList(Guid user, string name) { - var newList = new TodoListModel() { Name = name, Owner = Guid.Parse(user) }; + var newList = new TodoListModel() { Name = name, Owner = user }; _context.TodoLists.Add(newList); await _context.SaveChangesAsync(); return new TodoList() { Id = newList.Id, Name = newList.Name, Owner = newList.Owner }; } - public bool DeleteList(string userId, int id) + public bool DeleteList(Guid user, int id) { var list = _context.TodoLists - .SingleOrDefault(l => l.Owner.Equals(Guid.Parse(userId)) && l.Id == id); + .SingleOrDefault(l => l.Owner.Equals(user) && l.Id == id); if (list is null) { return false; } _context.TodoLists.Remove(list); _context.SaveChanges(); return true; } - public async Task RenameList(string userId, int id, string name) + public async Task RenameList(Guid user, int id, string name) { var list = _context.TodoLists - .SingleOrDefault(l => l.Owner.Equals(Guid.Parse(userId)) && l.Id == id); + .SingleOrDefault(l => l.Owner.Equals(user) && l.Id == id); if (list is null) { return false; } list.Name = name; @@ -63,12 +63,12 @@ public async Task RenameList(string userId, int id, string name) return true; } - public async Task CreateItem(string user, TodoItem item) + public async Task CreateItem(Guid user, TodoItem item) { var newItem = new TodoItemModel() { Name = item.Name, - Owner = Guid.Parse(user), + Owner = user, Compleated = false, Deadline = item.Deadline, Notes = item.Notes, @@ -80,7 +80,7 @@ public async Task CreateItem(string user, TodoItem item) var newSubtask = new TodoItemModel() { Name = st.Name, - Owner = Guid.Parse(user), + Owner = user, Compleated = false, Deadline = st.Deadline, Notes = st.Notes, @@ -91,10 +91,10 @@ public async Task CreateItem(string user, TodoItem item) } foreach (var tag in item.Tags) { - var t = _context.Tags.FirstOrDefault(t => t.Id == tag.Id && t.Owner.Equals(Guid.Parse(user))); + var t = _context.Tags.FirstOrDefault(t => t.Id == tag.Id && t.Owner.Equals(user)); if (t is null) { - newItem.Tags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = Guid.Parse(user) }); + newItem.Tags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = user }); } else { @@ -106,36 +106,36 @@ public async Task CreateItem(string user, TodoItem item) return item with { Id = newItem.Id }; } - public async Task GetItem(string user, int id) + public async Task GetItem(Guid user, int id) { var item = await _context.Todos .AsNoTracking() .Include(i => i.Subtasks) .Include(i => i.Tags) .AsSplitQuery() - .SingleOrDefaultAsync(i => i.Owner.Equals(Guid.Parse(user)) && i.Id == id); + .SingleOrDefaultAsync(i => i.Owner.Equals(user) && i.Id == id); if (item is null) { return null; } return MapTodoItem(item); } - public IEnumerable ListItems(string user) + public IEnumerable ListItems(Guid user) { return _context.Todos .AsNoTracking() .Include(i => i.Subtasks) .Include(i => i.Tags) - .Where(i => i.Owner.Equals(Guid.Parse(user)) && i.MainTaskId == null) + .Where(i => i.Owner.Equals(user) && i.MainTaskId == null) .AsSplitQuery() .Select(i => MapTodoItem(i)) .AsEnumerable(); } - public async Task DeleteItem(string user, int id) + public async Task DeleteItem(Guid user, int id) { var item = _context.Todos .Include(i => i.Subtasks) - .SingleOrDefault(i => i.Owner.Equals(Guid.Parse(user)) && i.Id == id); + .SingleOrDefault(i => i.Owner.Equals(user) && i.Id == id); if (item is null) { return false; } if (item.Subtasks.Any()) { @@ -146,13 +146,13 @@ public async Task DeleteItem(string user, int id) return true; } - public async Task UpdateItem(string user, TodoItem item) + public async Task UpdateItem(Guid user, TodoItem item) { var existingItem = _context.Todos .Include(i => i.Subtasks) .Include(i => i.Tags) .AsSplitQuery() - .SingleOrDefault(i => i.Owner.Equals(Guid.Parse(user)) && i.Id == item.Id); + .SingleOrDefault(i => i.Owner.Equals(user) && i.Id == item.Id); if (existingItem is null) { return false; } existingItem.Name = item.Name; existingItem.Deadline = item.Deadline; @@ -166,7 +166,7 @@ public async Task UpdateItem(string user, TodoItem item) var newSubtask = new TodoItemModel() { Name = st.Name, - Owner = Guid.Parse(user), + Owner = user, Compleated = st.Compleated, Deadline = st.Deadline, Notes = st.Notes, @@ -189,7 +189,7 @@ public async Task UpdateItem(string user, TodoItem item) var t = existingTags.FirstOrDefault(t => t.Id == tag.Id); if (t is null) { - existingTags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = Guid.Parse(user) }); + existingTags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = user }); } else { From bd3598f6022049f386cba4ccc9698b0250482204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 20:06:01 +0200 Subject: [PATCH 11/17] Closes #60 Rename and make requests nested class Add comment for UserId --- backend/src/api/Controllers/TodoController.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index 0aab511..15488bf 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -15,6 +15,8 @@ public class TodoController(ITodoService service, ILogger logger { private readonly ITodoService _service = service; private readonly ILogger _logger = logger; + + //We are sure that this is not null because of the [UserIdFilter] private Guid UserId => (Guid)HttpContext.Items["UserId"]!; [HttpGet("lists")] @@ -49,7 +51,7 @@ public IActionResult DeleteList([FromRoute] int id) } [HttpPost("lists")] - public async Task CreateLists([FromBody] Request request) + public async Task CreateLists([FromBody] ListCreationRequest request) { _logger.LogDebug("User: {user} created: {list}", UserId, request.Name); var list = await _service.CreateList(UserId, request.Name); @@ -57,7 +59,7 @@ public async Task CreateLists([FromBody] Request request) } [HttpPut("lists/{id}")] - public async Task RenameList(RenameRequest request) + public async Task RenameList(ListRenameRequest request) { _logger.LogDebug("User: {user} renamed: {id} to: {list}", UserId, request.Id, request.Body.Name); var list = await _service.RenameList(UserId, request.Id, request.Body.Name); @@ -122,22 +124,22 @@ public async Task UpdateItem([FromRoute] int id, [FromBody] TodoI } return BadRequest(); } -} - -public class Request -{ - public required string Name { get; set; } -} - -public class RenameRequest -{ - [FromRoute] - public required int Id { get; set; } - [FromBody] - public required Payload Body { get; set; } - public class Payload + public class ListCreationRequest { public required string Name { get; set; } } + + public class ListRenameRequest + { + [FromRoute] + public required int Id { get; set; } + [FromBody] + public required Payload Body { get; set; } + + public class Payload + { + public required string Name { get; set; } + } + } } From 9e11db157b1a2b8832750647838689f15acc0230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 21:06:01 +0200 Subject: [PATCH 12/17] Closes #60 Introduce Item - List mapping --- Makefile | 5 +- .../bruno/KSummarized/ToDo/Items/Create.bru | 3 +- backend/src/api/Responses/TodoList.cs | 6 +- backend/src/core/TodoItem.cs | 7 +- backend/src/core/TodoList.cs | 3 +- .../Data/ApplicationDbContext.cs | 2 +- backend/src/infrastructure/Data/Tag.cs | 2 +- .../src/infrastructure/Data/TodoItemModel.cs | 4 +- .../src/infrastructure/Data/TodoListModel.cs | 1 + .../src/infrastructure/Data/TodoService.cs | 33 +++- ...7182157_Introduce list mapping.Designer.cs | 171 ++++++++++++++++++ .../20240927182157_Introduce list mapping.cs | 114 ++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 28 ++- 13 files changed, 354 insertions(+), 25 deletions(-) create mode 100644 backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs create mode 100644 backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs diff --git a/Makefile b/Makefile index fbcc81a..f3e8949 100644 --- a/Makefile +++ b/Makefile @@ -4,5 +4,8 @@ dev: no-reload: docker compose --profile without-hot-reload up --build -.PHONY: dev no-reload +db: + docker compose up db -d + +.PHONY: dev no-reload db .DEFAULT_GOAL := dev diff --git a/backend/bruno/KSummarized/ToDo/Items/Create.bru b/backend/bruno/KSummarized/ToDo/Items/Create.bru index 7e50b18..fd2b3fc 100644 --- a/backend/bruno/KSummarized/ToDo/Items/Create.bru +++ b/backend/bruno/KSummarized/ToDo/Items/Create.bru @@ -44,7 +44,8 @@ body:json { "tags": [], "subtasks": [] } - ] + ], + "listId": {{listId}} } } diff --git a/backend/src/api/Responses/TodoList.cs b/backend/src/api/Responses/TodoList.cs index 9207e35..bc4beac 100644 --- a/backend/src/api/Responses/TodoList.cs +++ b/backend/src/api/Responses/TodoList.cs @@ -1,8 +1,10 @@ +using core; + namespace api.Resonses; -public record TodoList(int Id, string Name); +public record TodoList(int Id, string Name, IEnumerable Items); public static class MapExtensions { - public static TodoList ToResponse(this core.TodoList list) => new TodoList(list.Id, list.Name); + public static TodoList ToResponse(this core.TodoList list) => new TodoList(list.Id, list.Name, list.Items); } diff --git a/backend/src/core/TodoItem.cs b/backend/src/core/TodoItem.cs index dbc2f2e..dec6073 100644 --- a/backend/src/core/TodoItem.cs +++ b/backend/src/core/TodoItem.cs @@ -4,9 +4,10 @@ public record TodoItem { public int? Id { get; set; } public required string Name { get; set; } - public bool Compleated {get; set;} - public DateTime Deadline {get; set;} + public bool Compleated { get; set; } + public DateTime Deadline { get; set; } public string Notes { get; set; } = null!; public required IEnumerable Tags { get; set; } public required IEnumerable Subtasks { get; set; } -} \ No newline at end of file + public int ListId { get; set; } +} diff --git a/backend/src/core/TodoList.cs b/backend/src/core/TodoList.cs index 8392f04..6fe7c76 100644 --- a/backend/src/core/TodoList.cs +++ b/backend/src/core/TodoList.cs @@ -5,6 +5,5 @@ public class TodoList public int Id { get; set; } public required string Name { get; set; } public required Guid Owner { get; set; } - - public bool IsAuthorized(Guid person) => Owner.Equals(person); + public required IEnumerable Items { get; set; } } diff --git a/backend/src/infrastructure/Data/ApplicationDbContext.cs b/backend/src/infrastructure/Data/ApplicationDbContext.cs index f091328..5e9e228 100644 --- a/backend/src/infrastructure/Data/ApplicationDbContext.cs +++ b/backend/src/infrastructure/Data/ApplicationDbContext.cs @@ -6,5 +6,5 @@ public class ApplicationDbContext(DbContextOptions options { public required DbSet TodoLists { get; set; } public required DbSet Todos { get; set; } - public required DbSet Tags { get; set; } + public required DbSet Tags { get; set; } } diff --git a/backend/src/infrastructure/Data/Tag.cs b/backend/src/infrastructure/Data/Tag.cs index 78bd2fa..48f2c3a 100644 --- a/backend/src/infrastructure/Data/Tag.cs +++ b/backend/src/infrastructure/Data/Tag.cs @@ -4,7 +4,7 @@ namespace infrastructure.Data; [Table("Tags")] -public class Tag +public class TagModel { [Key] public int Id { get; set; } diff --git a/backend/src/infrastructure/Data/TodoItemModel.cs b/backend/src/infrastructure/Data/TodoItemModel.cs index d3d1989..a9e976d 100644 --- a/backend/src/infrastructure/Data/TodoItemModel.cs +++ b/backend/src/infrastructure/Data/TodoItemModel.cs @@ -13,8 +13,10 @@ public class TodoItemModel public DateTime Deadline { get; set; } [MaxLength(4096)] public string Notes { get; set; } = null!; - public required ICollection Tags { get; set; } + public required ICollection Tags { get; set; } public required ICollection Subtasks { get; set; } public int? MainTaskId { get; set; } public TodoItemModel? MainTask { get; set; } + public int ListId { get; set; } + public TodoListModel? List { get; set; } } diff --git a/backend/src/infrastructure/Data/TodoListModel.cs b/backend/src/infrastructure/Data/TodoListModel.cs index d838b5b..5494087 100644 --- a/backend/src/infrastructure/Data/TodoListModel.cs +++ b/backend/src/infrastructure/Data/TodoListModel.cs @@ -11,4 +11,5 @@ public class TodoListModel [MaxLength(512)] public required string Name { get; set; } public required Guid Owner { get; set; } + public required ICollection Items { get; set; } } diff --git a/backend/src/infrastructure/Data/TodoService.cs b/backend/src/infrastructure/Data/TodoService.cs index 6e08cf9..38b8717 100644 --- a/backend/src/infrastructure/Data/TodoService.cs +++ b/backend/src/infrastructure/Data/TodoService.cs @@ -21,25 +21,40 @@ public IEnumerable GetLists(Guid user) { return _context.TodoLists.AsNoTracking() .Where(list => list.Owner.Equals(user)) - .Select(list => new TodoList { Id = list.Id, Name = list.Name, Owner = list.Owner }) + .Select(list => new TodoList + { + Id = list.Id, + Name = list.Name, + Owner = list.Owner, + Items = Enumerable.Empty() + }) .AsEnumerable(); } public TodoList? GetList(Guid user, int id) { var list = _context.TodoLists.AsNoTracking() + .Include(l => l.Items) .SingleOrDefault(l => l.Owner.Equals(user) && l.Id == id); - //TODO: Consider creating a mapper instead of this manual new - if (list is not null) { return new() { Id = list.Id, Name = list.Name, Owner = list.Owner }; } + if (list is not null) + { + return new() + { + Id = list.Id, + Name = list.Name, + Owner = list.Owner, + Items = list.Items.Select(i => MapTodoItem(i)).ToList() + }; + }; return null; } public async Task CreateList(Guid user, string name) { - var newList = new TodoListModel() { Name = name, Owner = user }; + var newList = new TodoListModel() { Name = name, Owner = user, Items = [] }; _context.TodoLists.Add(newList); await _context.SaveChangesAsync(); - return new TodoList() { Id = newList.Id, Name = newList.Name, Owner = newList.Owner }; + return new TodoList() { Id = newList.Id, Name = newList.Name, Owner = newList.Owner, Items = [] }; } public bool DeleteList(Guid user, int id) @@ -73,7 +88,8 @@ public async Task CreateItem(Guid user, TodoItem item) Deadline = item.Deadline, Notes = item.Notes, Subtasks = [], - Tags = [] + Tags = [], + ListId = item.ListId }; foreach (var st in item.Subtasks) { @@ -85,7 +101,8 @@ public async Task CreateItem(Guid user, TodoItem item) Deadline = st.Deadline, Notes = st.Notes, Tags = [], - Subtasks = [] + Subtasks = [], + ListId = item.ListId }; newItem.Subtasks.Add(newSubtask); } @@ -204,7 +221,7 @@ public async Task UpdateItem(Guid user, TodoItem item) private static TodoItem MapTodoItem(infrastructure.Data.TodoItemModel item) { - Log.Information("Mapped {item}", JsonSerializer.Serialize(item, new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve })); + Log.Debug("Mapped {item}", JsonSerializer.Serialize(item, new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve })); return new TodoItem() { Id = item.Id, diff --git a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs new file mode 100644 index 0000000..a9e8f50 --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using infrastructure.Data; + +#nullable disable + +namespace infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240927182157_Introduce list mapping")] + partial class Introducelistmapping + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TagModelTodoItemModel", b => + { + b.Property("ItemsId") + .HasColumnType("integer"); + + b.Property("TagsId") + .HasColumnType("integer"); + + b.HasKey("ItemsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("TagModelTodoItemModel"); + }); + + modelBuilder.Entity("infrastructure.Data.TagModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Compleated") + .HasColumnType("boolean"); + + b.Property("Deadline") + .HasColumnType("timestamp with time zone"); + + b.Property("ListId") + .HasColumnType("integer"); + + b.Property("MainTaskId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ListId"); + + b.HasIndex("MainTaskId"); + + b.ToTable("Todos"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("todo_lists"); + }); + + modelBuilder.Entity("TagModelTodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.TagModel", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoListModel", "List") + .WithMany("Items") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.TodoItemModel", "MainTask") + .WithMany("Subtasks") + .HasForeignKey("MainTaskId"); + + b.Navigation("List"); + + b.Navigation("MainTask"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Navigation("Subtasks"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs new file mode 100644 index 0000000..332ebf1 --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace infrastructure.Migrations +{ + /// + public partial class Introducelistmapping : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TagTodoItemModel"); + + migrationBuilder.AddColumn( + name: "ListId", + table: "Todos", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "TagModelTodoItemModel", + columns: table => new + { + ItemsId = table.Column(type: "integer", nullable: false), + TagsId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TagModelTodoItemModel", x => new { x.ItemsId, x.TagsId }); + table.ForeignKey( + name: "FK_TagModelTodoItemModel_Tags_TagsId", + column: x => x.TagsId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TagModelTodoItemModel_Todos_ItemsId", + column: x => x.ItemsId, + principalTable: "Todos", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Todos_ListId", + table: "Todos", + column: "ListId"); + + migrationBuilder.CreateIndex( + name: "IX_TagModelTodoItemModel_TagsId", + table: "TagModelTodoItemModel", + column: "TagsId"); + + migrationBuilder.AddForeignKey( + name: "FK_Todos_todo_lists_ListId", + table: "Todos", + column: "ListId", + principalTable: "todo_lists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Todos_todo_lists_ListId", + table: "Todos"); + + migrationBuilder.DropTable( + name: "TagModelTodoItemModel"); + + migrationBuilder.DropIndex( + name: "IX_Todos_ListId", + table: "Todos"); + + migrationBuilder.DropColumn( + name: "ListId", + table: "Todos"); + + migrationBuilder.CreateTable( + name: "TagTodoItemModel", + columns: table => new + { + ItemsId = table.Column(type: "integer", nullable: false), + TagsId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TagTodoItemModel", x => new { x.ItemsId, x.TagsId }); + table.ForeignKey( + name: "FK_TagTodoItemModel_Tags_TagsId", + column: x => x.TagsId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TagTodoItemModel_Todos_ItemsId", + column: x => x.ItemsId, + principalTable: "Todos", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TagTodoItemModel_TagsId", + table: "TagTodoItemModel", + column: "TagsId"); + } + } +} diff --git a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 399b238..fb4e1d9 100644 --- a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,7 +22,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("TagTodoItemModel", b => + modelBuilder.Entity("TagModelTodoItemModel", b => { b.Property("ItemsId") .HasColumnType("integer"); @@ -34,10 +34,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("TagsId"); - b.ToTable("TagTodoItemModel"); + b.ToTable("TagModelTodoItemModel"); }); - modelBuilder.Entity("infrastructure.Data.Tag", b => + modelBuilder.Entity("infrastructure.Data.TagModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -72,6 +72,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Deadline") .HasColumnType("timestamp with time zone"); + b.Property("ListId") + .HasColumnType("integer"); + b.Property("MainTaskId") .HasColumnType("integer"); @@ -90,6 +93,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("ListId"); + b.HasIndex("MainTaskId"); b.ToTable("Todos"); @@ -116,7 +121,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("todo_lists"); }); - modelBuilder.Entity("TagTodoItemModel", b => + modelBuilder.Entity("TagModelTodoItemModel", b => { b.HasOne("infrastructure.Data.TodoItemModel", null) .WithMany() @@ -124,7 +129,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("infrastructure.Data.Tag", null) + b.HasOne("infrastructure.Data.TagModel", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) @@ -133,10 +138,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => { + b.HasOne("infrastructure.Data.TodoListModel", "List") + .WithMany("Items") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.HasOne("infrastructure.Data.TodoItemModel", "MainTask") .WithMany("Subtasks") .HasForeignKey("MainTaskId"); + b.Navigation("List"); + b.Navigation("MainTask"); }); @@ -144,6 +157,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Subtasks"); }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Navigation("Items"); + }); #pragma warning restore 612, 618 } } From cd3369d1476eca59ef866c3b7b4fb43fc66d0b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 21:16:17 +0200 Subject: [PATCH 13/17] Closes #60 Rename file to match class name --- backend/src/infrastructure/Data/{Tag.cs => TagModel.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/infrastructure/Data/{Tag.cs => TagModel.cs} (100%) diff --git a/backend/src/infrastructure/Data/Tag.cs b/backend/src/infrastructure/Data/TagModel.cs similarity index 100% rename from backend/src/infrastructure/Data/Tag.cs rename to backend/src/infrastructure/Data/TagModel.cs From 5eea349e1081536dd4c6046828e92ec33cebc4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 21:22:00 +0200 Subject: [PATCH 14/17] Closes #60 Add missing list change in update of an item --- backend/bruno/KSummarized/ToDo/Items/Update.bru | 3 ++- backend/src/infrastructure/Data/TodoService.cs | 13 ++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/bruno/KSummarized/ToDo/Items/Update.bru b/backend/bruno/KSummarized/ToDo/Items/Update.bru index e60d720..5fd77ee 100644 --- a/backend/bruno/KSummarized/ToDo/Items/Update.bru +++ b/backend/bruno/KSummarized/ToDo/Items/Update.bru @@ -41,6 +41,7 @@ body:json { "tags": [], "subtasks": [] } - ] + ], + "listId": 2 } } diff --git a/backend/src/infrastructure/Data/TodoService.cs b/backend/src/infrastructure/Data/TodoService.cs index 38b8717..0237fc2 100644 --- a/backend/src/infrastructure/Data/TodoService.cs +++ b/backend/src/infrastructure/Data/TodoService.cs @@ -175,6 +175,7 @@ public async Task UpdateItem(Guid user, TodoItem item) existingItem.Deadline = item.Deadline; existingItem.Notes = item.Notes; existingItem.Compleated = item.Compleated; + existingItem.ListId = item.ListId; foreach (var st in item.Subtasks) { var existingSubtask = existingItem.Subtasks.FirstOrDefault(st => st.Id == st.Id); @@ -188,7 +189,8 @@ public async Task UpdateItem(Guid user, TodoItem item) Deadline = st.Deadline, Notes = st.Notes, Tags = [], - Subtasks = [] + Subtasks = [], + ListId = item.ListId }; existingItem.Subtasks.Add(newSubtask); } @@ -198,6 +200,7 @@ public async Task UpdateItem(Guid user, TodoItem item) existingSubtask.Deadline = st.Deadline; existingSubtask.Notes = st.Notes; existingSubtask.Compleated = st.Compleated; + existingSubtask.ListId = item.ListId; } } var existingTags = existingItem.Tags.ToList(); @@ -229,7 +232,9 @@ private static TodoItem MapTodoItem(infrastructure.Data.TodoItemModel item) Deadline = item.Deadline, Notes = item.Notes, Subtasks = item.Subtasks?.Select(st => MapSubtask(st)).ToList() ?? [], - Tags = item.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [] + Tags = item.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [], + ListId = item.ListId, + Compleated = item.Compleated }; } @@ -242,7 +247,9 @@ private static TodoItem MapSubtask(infrastructure.Data.TodoItemModel subtask) Deadline = subtask.Deadline, Notes = subtask.Notes, Subtasks = [], - Tags = subtask.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [] + Tags = subtask.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [], + ListId = subtask.ListId, + Compleated = subtask.Compleated }; } } From a331de9c75d528461ba65714723539a90dd30a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Fri, 27 Sep 2024 21:24:14 +0200 Subject: [PATCH 15/17] Closes #60 Fix typo --- backend/src/infrastructure/Data/TodoService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/infrastructure/Data/TodoService.cs b/backend/src/infrastructure/Data/TodoService.cs index 0237fc2..63ed420 100644 --- a/backend/src/infrastructure/Data/TodoService.cs +++ b/backend/src/infrastructure/Data/TodoService.cs @@ -178,7 +178,7 @@ public async Task UpdateItem(Guid user, TodoItem item) existingItem.ListId = item.ListId; foreach (var st in item.Subtasks) { - var existingSubtask = existingItem.Subtasks.FirstOrDefault(st => st.Id == st.Id); + var existingSubtask = existingItem.Subtasks.FirstOrDefault(t => t.Id == st.Id); if (existingSubtask is null) { var newSubtask = new TodoItemModel() From c2ba0a049b3822ac910cda3b2646557fc75ff463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Tue, 1 Oct 2024 16:56:13 +0200 Subject: [PATCH 16/17] Closes #60 Addressing PR comments --- Makefile | 9 ++++- README.md | 2 +- .../bruno/KSummarized/ToDo/Items/Create.bru | 6 ++-- .../bruno/KSummarized/ToDo/Items/Delete.bru | 31 ---------------- backend/bruno/KSummarized/ToDo/Items/Get.bru | 35 ------------------- backend/bruno/KSummarized/ToDo/Items/List.bru | 31 ---------------- backend/src/api/Controllers/TodoController.cs | 2 +- backend/src/api/Responses/TodoList.cs | 2 +- backend/src/core/TodoItem.cs | 2 +- .../Data/ApplicationDbContext.cs | 2 +- .../src/infrastructure/Data/TodoItemModel.cs | 4 ++- .../src/infrastructure/Data/TodoListModel.cs | 2 +- .../src/infrastructure/Data/TodoService.cs | 31 ++++++++-------- .../src/infrastructure/LoggingExtensions.cs | 1 - .../20240330212015_TodoList.Designer.cs | 2 +- .../Migrations/20240330212015_TodoList.cs | 6 ++-- .../20240920175610_Add items.Designer.cs | 8 ++--- .../Migrations/20240920175610_Add items.cs | 14 ++++---- ...7182157_Introduce list mapping.Designer.cs | 8 ++--- .../20240927182157_Introduce list mapping.cs | 24 ++++++------- .../ApplicationDbContextModelSnapshot.cs | 8 ++--- 21 files changed, 71 insertions(+), 159 deletions(-) delete mode 100644 backend/src/infrastructure/LoggingExtensions.cs diff --git a/Makefile b/Makefile index f3e8949..defe95e 100644 --- a/Makefile +++ b/Makefile @@ -7,5 +7,12 @@ no-reload: db: docker compose up db -d -.PHONY: dev no-reload db +db_down: + docker compose down db + +apply_migrations: + @cd scripts && pwsh apply_migrations.ps1 + @cd ../ + +.PHONY: dev no-reload db db_down apply_migrations .DEFAULT_GOAL := dev diff --git a/README.md b/README.md index e668445..1e2cc6e 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ The application is started using the following command (with hot-reload feature) ```bash make ``` -or with the hot-reload feature: +or without the hot-reload feature: ```bash make no-reload ``` diff --git a/backend/bruno/KSummarized/ToDo/Items/Create.bru b/backend/bruno/KSummarized/ToDo/Items/Create.bru index fd2b3fc..a40a8b9 100644 --- a/backend/bruno/KSummarized/ToDo/Items/Create.bru +++ b/backend/bruno/KSummarized/ToDo/Items/Create.bru @@ -18,7 +18,7 @@ body:json { { "id": 1, "name": "Demo", - "compleated": false, + "completed": false, "deadline": "2024-09-20T18:27:39.149Z", "notes": "hello there", "tags": [{ @@ -29,7 +29,7 @@ body:json { { "id": 2, "name": "Demo", - "compleated": false, + "completed": false, "deadline": "2024-09-20T18:27:39.149Z", "notes": "hello there2", "tags": [], @@ -38,7 +38,7 @@ body:json { { "id": 3, "name": "Demo3", - "compleated": false, + "completed": false, "deadline": "2024-09-20T18:27:39.149Z", "notes": "hello there 3", "tags": [], diff --git a/backend/bruno/KSummarized/ToDo/Items/Delete.bru b/backend/bruno/KSummarized/ToDo/Items/Delete.bru index eebea05..8adb760 100644 --- a/backend/bruno/KSummarized/ToDo/Items/Delete.bru +++ b/backend/bruno/KSummarized/ToDo/Items/Delete.bru @@ -13,34 +13,3 @@ delete { auth:bearer { token: {{token}} } - -body:json { - { - "id": 1, - "name": "Demo-Updated", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there", - "tags": [], - "subtasks": [ - { - "id": 2, - "name": "Demo2", - "compleated": true, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there2", - "tags": [], - "subtasks": [] - }, - { - "id": 3, - "name": "Demo22", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there 3", - "tags": [], - "subtasks": [] - } - ] - } -} diff --git a/backend/bruno/KSummarized/ToDo/Items/Get.bru b/backend/bruno/KSummarized/ToDo/Items/Get.bru index 7576768..7e903e8 100644 --- a/backend/bruno/KSummarized/ToDo/Items/Get.bru +++ b/backend/bruno/KSummarized/ToDo/Items/Get.bru @@ -13,38 +13,3 @@ get { auth:bearer { token: {{token}} } - -body:json { - { - "id": 1, - "name": "Demo", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there", - "tags": [], - "subtasks": [ - { - "id": 2, - "name": "Demo", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there2", - "tags": [], - "subtasks": [] - }, - { - "id": 3, - "name": "Demo3", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there 3", - "tags": [], - "subtasks": [] - } - ] - } -} - -vars:post-response { - listId: res.body.id -} diff --git a/backend/bruno/KSummarized/ToDo/Items/List.bru b/backend/bruno/KSummarized/ToDo/Items/List.bru index c11a33e..51fddeb 100644 --- a/backend/bruno/KSummarized/ToDo/Items/List.bru +++ b/backend/bruno/KSummarized/ToDo/Items/List.bru @@ -13,34 +13,3 @@ get { auth:bearer { token: {{token}} } - -body:json { - { - "id": 1, - "name": "Demo", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there", - "tags": [], - "subtasks": [ - { - "id": 2, - "name": "Demo", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there2", - "tags": [], - "subtasks": [] - }, - { - "id": 3, - "name": "Demo3", - "compleated": false, - "deadline": "2024-09-20T18:27:39.149Z", - "notes": "hello there 3", - "tags": [], - "subtasks": [] - } - ] - } -} diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index 15488bf..e3a1d53 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -1,7 +1,7 @@ using core.Ports; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using api.Resonses; +using api.Responses; using api.Filters; using core; diff --git a/backend/src/api/Responses/TodoList.cs b/backend/src/api/Responses/TodoList.cs index bc4beac..ba73c85 100644 --- a/backend/src/api/Responses/TodoList.cs +++ b/backend/src/api/Responses/TodoList.cs @@ -1,6 +1,6 @@ using core; -namespace api.Resonses; +namespace api.Responses; public record TodoList(int Id, string Name, IEnumerable Items); diff --git a/backend/src/core/TodoItem.cs b/backend/src/core/TodoItem.cs index dec6073..082b46d 100644 --- a/backend/src/core/TodoItem.cs +++ b/backend/src/core/TodoItem.cs @@ -4,7 +4,7 @@ public record TodoItem { public int? Id { get; set; } public required string Name { get; set; } - public bool Compleated { get; set; } + public bool Completed { get; set; } public DateTime Deadline { get; set; } public string Notes { get; set; } = null!; public required IEnumerable Tags { get; set; } diff --git a/backend/src/infrastructure/Data/ApplicationDbContext.cs b/backend/src/infrastructure/Data/ApplicationDbContext.cs index 5e9e228..383126f 100644 --- a/backend/src/infrastructure/Data/ApplicationDbContext.cs +++ b/backend/src/infrastructure/Data/ApplicationDbContext.cs @@ -5,6 +5,6 @@ namespace infrastructure.Data; public class ApplicationDbContext(DbContextOptions options) : DbContext(options) { public required DbSet TodoLists { get; set; } - public required DbSet Todos { get; set; } + public required DbSet TodoItems { get; set; } public required DbSet Tags { get; set; } } diff --git a/backend/src/infrastructure/Data/TodoItemModel.cs b/backend/src/infrastructure/Data/TodoItemModel.cs index a9e976d..7ac8d76 100644 --- a/backend/src/infrastructure/Data/TodoItemModel.cs +++ b/backend/src/infrastructure/Data/TodoItemModel.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace infrastructure.Data; +[Table("TodoItems")] public class TodoItemModel { [Key] @@ -9,7 +11,7 @@ public class TodoItemModel [MaxLength(512)] public required string Name { get; set; } public required Guid Owner { get; set; } - public bool Compleated { get; set; } + public bool Completed { get; set; } public DateTime Deadline { get; set; } [MaxLength(4096)] public string Notes { get; set; } = null!; diff --git a/backend/src/infrastructure/Data/TodoListModel.cs b/backend/src/infrastructure/Data/TodoListModel.cs index 5494087..70c3764 100644 --- a/backend/src/infrastructure/Data/TodoListModel.cs +++ b/backend/src/infrastructure/Data/TodoListModel.cs @@ -3,7 +3,7 @@ namespace infrastructure.Data; -[Table("todo_lists")] +[Table("TodoLists")] public class TodoListModel { [Key] diff --git a/backend/src/infrastructure/Data/TodoService.cs b/backend/src/infrastructure/Data/TodoService.cs index 63ed420..f72adf1 100644 --- a/backend/src/infrastructure/Data/TodoService.cs +++ b/backend/src/infrastructure/Data/TodoService.cs @@ -19,6 +19,7 @@ public TodoService(ApplicationDbContext context) public IEnumerable GetLists(Guid user) { + var empty = Enumerable.Empty(); return _context.TodoLists.AsNoTracking() .Where(list => list.Owner.Equals(user)) .Select(list => new TodoList @@ -26,7 +27,7 @@ public IEnumerable GetLists(Guid user) Id = list.Id, Name = list.Name, Owner = list.Owner, - Items = Enumerable.Empty() + Items = empty }) .AsEnumerable(); } @@ -84,7 +85,7 @@ public async Task CreateItem(Guid user, TodoItem item) { Name = item.Name, Owner = user, - Compleated = false, + Completed = false, Deadline = item.Deadline, Notes = item.Notes, Subtasks = [], @@ -97,7 +98,7 @@ public async Task CreateItem(Guid user, TodoItem item) { Name = st.Name, Owner = user, - Compleated = false, + Completed = false, Deadline = st.Deadline, Notes = st.Notes, Tags = [], @@ -118,14 +119,14 @@ public async Task CreateItem(Guid user, TodoItem item) t.Name = tag.Name; } } - await _context.Todos.AddAsync(newItem); + await _context.TodoItems.AddAsync(newItem); await _context.SaveChangesAsync(); return item with { Id = newItem.Id }; } public async Task GetItem(Guid user, int id) { - var item = await _context.Todos + var item = await _context.TodoItems .AsNoTracking() .Include(i => i.Subtasks) .Include(i => i.Tags) @@ -138,7 +139,7 @@ public async Task CreateItem(Guid user, TodoItem item) public IEnumerable ListItems(Guid user) { - return _context.Todos + return _context.TodoItems .AsNoTracking() .Include(i => i.Subtasks) .Include(i => i.Tags) @@ -150,22 +151,22 @@ public IEnumerable ListItems(Guid user) public async Task DeleteItem(Guid user, int id) { - var item = _context.Todos + var item = _context.TodoItems .Include(i => i.Subtasks) .SingleOrDefault(i => i.Owner.Equals(user) && i.Id == id); if (item is null) { return false; } if (item.Subtasks.Any()) { - _context.Todos.RemoveRange(item.Subtasks); + _context.TodoItems.RemoveRange(item.Subtasks); } - _context.Todos.Remove(item); + _context.TodoItems.Remove(item); await _context.SaveChangesAsync(); return true; } public async Task UpdateItem(Guid user, TodoItem item) { - var existingItem = _context.Todos + var existingItem = _context.TodoItems .Include(i => i.Subtasks) .Include(i => i.Tags) .AsSplitQuery() @@ -174,7 +175,7 @@ public async Task UpdateItem(Guid user, TodoItem item) existingItem.Name = item.Name; existingItem.Deadline = item.Deadline; existingItem.Notes = item.Notes; - existingItem.Compleated = item.Compleated; + existingItem.Completed = item.Completed; existingItem.ListId = item.ListId; foreach (var st in item.Subtasks) { @@ -185,7 +186,7 @@ public async Task UpdateItem(Guid user, TodoItem item) { Name = st.Name, Owner = user, - Compleated = st.Compleated, + Completed = st.Completed, Deadline = st.Deadline, Notes = st.Notes, Tags = [], @@ -199,7 +200,7 @@ public async Task UpdateItem(Guid user, TodoItem item) existingSubtask.Name = st.Name; existingSubtask.Deadline = st.Deadline; existingSubtask.Notes = st.Notes; - existingSubtask.Compleated = st.Compleated; + existingSubtask.Completed = st.Completed; existingSubtask.ListId = item.ListId; } } @@ -234,7 +235,7 @@ private static TodoItem MapTodoItem(infrastructure.Data.TodoItemModel item) Subtasks = item.Subtasks?.Select(st => MapSubtask(st)).ToList() ?? [], Tags = item.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [], ListId = item.ListId, - Compleated = item.Compleated + Completed = item.Completed }; } @@ -249,7 +250,7 @@ private static TodoItem MapSubtask(infrastructure.Data.TodoItemModel subtask) Subtasks = [], Tags = subtask.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [], ListId = subtask.ListId, - Compleated = subtask.Compleated + Completed = subtask.Completed }; } } diff --git a/backend/src/infrastructure/LoggingExtensions.cs b/backend/src/infrastructure/LoggingExtensions.cs deleted file mode 100644 index 8b13789..0000000 --- a/backend/src/infrastructure/LoggingExtensions.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs b/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs index ba9fe02..b18a79b 100644 --- a/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs +++ b/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs @@ -43,7 +43,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("todo_lists"); + b.ToTable("TodoLists"); }); #pragma warning restore 612, 618 } diff --git a/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs b/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs index 13b4cef..040f2a7 100644 --- a/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs +++ b/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs @@ -13,7 +13,7 @@ public partial class TodoList : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "todo_lists", + name: "TodoLists", columns: table => new { Id = table.Column(type: "integer", nullable: false) @@ -23,7 +23,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_todo_lists", x => x.Id); + table.PrimaryKey("PK_TodoLists", x => x.Id); }); } @@ -31,7 +31,7 @@ protected override void Up(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "todo_lists"); + name: "TodoLists"); } } } diff --git a/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs b/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs index 05b521b..8060e54 100644 --- a/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs +++ b/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -69,7 +69,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("Compleated") + b.Property("Completed") .HasColumnType("boolean"); b.Property("Deadline") @@ -95,7 +95,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("MainTaskId"); - b.ToTable("Todos"); + b.ToTable("TodoItems"); }); modelBuilder.Entity("infrastructure.Data.TodoListModel", b => @@ -116,7 +116,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("todo_lists"); + b.ToTable("TodoLists"); }); modelBuilder.Entity("TagTodoItemModel", b => diff --git a/backend/src/infrastructure/Migrations/20240920175610_Add items.cs b/backend/src/infrastructure/Migrations/20240920175610_Add items.cs index 7278e61..c995dc9 100644 --- a/backend/src/infrastructure/Migrations/20240920175610_Add items.cs +++ b/backend/src/infrastructure/Migrations/20240920175610_Add items.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -27,14 +27,14 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateTable( - name: "Todos", + name: "TodoItems", columns: table => new { Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), Owner = table.Column(type: "uuid", nullable: false), - Compleated = table.Column(type: "boolean", nullable: false), + Completed = table.Column(type: "boolean", nullable: false), Deadline = table.Column(type: "timestamp with time zone", nullable: false), Notes = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), MainTaskId = table.Column(type: "integer", nullable: true) @@ -45,7 +45,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table.ForeignKey( name: "FK_Todos_Todos_MainTaskId", column: x => x.MainTaskId, - principalTable: "Todos", + principalTable: "TodoItems", principalColumn: "Id"); }); @@ -68,7 +68,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table.ForeignKey( name: "FK_TagTodoItemModel_Todos_ItemsId", column: x => x.ItemsId, - principalTable: "Todos", + principalTable: "TodoItems", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); @@ -80,7 +80,7 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.CreateIndex( name: "IX_Todos_MainTaskId", - table: "Todos", + table: "TodoItems", column: "MainTaskId"); } @@ -94,7 +94,7 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "Tags"); migrationBuilder.DropTable( - name: "Todos"); + name: "TodoItems"); } } } diff --git a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs index a9e8f50..fa3a895 100644 --- a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs +++ b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -69,7 +69,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("Compleated") + b.Property("Completed") .HasColumnType("boolean"); b.Property("Deadline") @@ -100,7 +100,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("MainTaskId"); - b.ToTable("Todos"); + b.ToTable("TodoItems"); }); modelBuilder.Entity("infrastructure.Data.TodoListModel", b => @@ -121,7 +121,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("todo_lists"); + b.ToTable("TodoLists"); }); modelBuilder.Entity("TagModelTodoItemModel", b => diff --git a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs index 332ebf1..29f0354 100644 --- a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs +++ b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -15,7 +15,7 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.AddColumn( name: "ListId", - table: "Todos", + table: "TodoItems", type: "integer", nullable: false, defaultValue: 0); @@ -39,14 +39,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.ForeignKey( name: "FK_TagModelTodoItemModel_Todos_ItemsId", column: x => x.ItemsId, - principalTable: "Todos", + principalTable: "TodoItems", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( name: "IX_Todos_ListId", - table: "Todos", + table: "TodoItems", column: "ListId"); migrationBuilder.CreateIndex( @@ -55,10 +55,10 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "TagsId"); migrationBuilder.AddForeignKey( - name: "FK_Todos_todo_lists_ListId", - table: "Todos", + name: "FK_Todos_TodoLists_ListId", + table: "TodoItems", column: "ListId", - principalTable: "todo_lists", + principalTable: "TodoLists", principalColumn: "Id", onDelete: ReferentialAction.Cascade); } @@ -67,19 +67,19 @@ protected override void Up(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropForeignKey( - name: "FK_Todos_todo_lists_ListId", - table: "Todos"); + name: "FK_Todos_TodoLists_ListId", + table: "TodoItems"); migrationBuilder.DropTable( name: "TagModelTodoItemModel"); migrationBuilder.DropIndex( name: "IX_Todos_ListId", - table: "Todos"); + table: "TodoItems"); migrationBuilder.DropColumn( name: "ListId", - table: "Todos"); + table: "TodoItems"); migrationBuilder.CreateTable( name: "TagTodoItemModel", @@ -100,7 +100,7 @@ protected override void Down(MigrationBuilder migrationBuilder) table.ForeignKey( name: "FK_TagTodoItemModel_Todos_ItemsId", column: x => x.ItemsId, - principalTable: "Todos", + principalTable: "TodoItems", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); diff --git a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index fb4e1d9..11821ce 100644 --- a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -66,7 +66,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("Compleated") + b.Property("Completed") .HasColumnType("boolean"); b.Property("Deadline") @@ -97,7 +97,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("MainTaskId"); - b.ToTable("Todos"); + b.ToTable("TodoItems"); }); modelBuilder.Entity("infrastructure.Data.TodoListModel", b => @@ -118,7 +118,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("todo_lists"); + b.ToTable("TodoLists"); }); modelBuilder.Entity("TagModelTodoItemModel", b => From a6818eafd6a89865503714c3af2948593f71bab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tracewicz?= Date: Tue, 1 Oct 2024 17:09:41 +0200 Subject: [PATCH 17/17] Closes #60 Addressing PR issues --- backend/src/api/Controllers/TodoController.cs | 2 +- backend/src/api/Filters/UserIdFilter.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index e3a1d53..b047ccb 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -34,7 +34,7 @@ public IActionResult GetList([FromRoute] int id) return list switch { null => NotFound(), - var user => Ok(list), + var l => Ok(l), }; } diff --git a/backend/src/api/Filters/UserIdFilter.cs b/backend/src/api/Filters/UserIdFilter.cs index d7b8bbb..d7b5506 100644 --- a/backend/src/api/Filters/UserIdFilter.cs +++ b/backend/src/api/Filters/UserIdFilter.cs @@ -3,6 +3,7 @@ namespace api.Filters; +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class UserIdFilter : Attribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context)