diff --git a/api/Controllers/CommentController.cs b/api/Controllers/CommentController.cs index 8ffdd24..ded349c 100644 --- a/api/Controllers/CommentController.cs +++ b/api/Controllers/CommentController.cs @@ -1,6 +1,9 @@ using api.DTOs.Comment; +using api.Extensions; using api.Interfaces; using api.Mappers; +using api.Models; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace api.Controllers @@ -11,11 +14,13 @@ public class CommentController : ControllerBase { private readonly ICommentRepo _commentRepo; private readonly IStockRepo _stockRepo; + private readonly UserManager _userManager; - public CommentController(ICommentRepo commentRepo, IStockRepo stockRepo) + public CommentController(ICommentRepo commentRepo, IStockRepo stockRepo, UserManager userManager) { _commentRepo = commentRepo; _stockRepo = stockRepo; + _userManager = userManager; } [HttpGet] @@ -71,7 +76,21 @@ public async Task CreateComment([FromRoute] int stockId, [FromBod return BadRequest("Stock does not exist."); } - var commentModel = commentDto.FromCreatedDtoToComment(stockId); + var username = User.GetUsername(); + + if (username == null) + { + return Unauthorized(); + } + + var user = await _userManager.FindByNameAsync(username); + + if (user == null) + { + return NotFound(); + } + + var commentModel = commentDto.FromCreatedDtoToComment(stockId, user.Id); await _commentRepo.CreateAsync(commentModel); diff --git a/api/DTOs/Comment/CommentDto.cs b/api/DTOs/Comment/CommentDto.cs index 3ae8f65..ae84719 100644 --- a/api/DTOs/Comment/CommentDto.cs +++ b/api/DTOs/Comment/CommentDto.cs @@ -6,6 +6,7 @@ public class CommentDto public string Title { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; public DateTime CreatedOn { get; set; } + public string? CreatedBy { get; set; } = string.Empty; public int? StockId { get; set; } } } \ No newline at end of file diff --git a/api/Mappers/CommentMappers.cs b/api/Mappers/CommentMappers.cs index 812dad9..438fdc3 100644 --- a/api/Mappers/CommentMappers.cs +++ b/api/Mappers/CommentMappers.cs @@ -13,17 +13,19 @@ public static CommentDto ToCommentDto(this Comment commentModel) Title = commentModel.Title, Content = commentModel.Content, CreatedOn = commentModel.CreatedOn, - StockId = commentModel.StockId + CreatedBy = commentModel.AppUser?.UserName, + StockId = commentModel.StockId, }; } - public static Comment FromCreatedDtoToComment(this CreateCommentRequestDto commentDto, int stockId) + public static Comment FromCreatedDtoToComment(this CreateCommentRequestDto commentDto, int stockId, string userId) { return new Comment { Title = commentDto.Title, Content = commentDto.Content, StockId = stockId, + AppUserId = userId }; } diff --git a/api/Migrations/20240626013021_AppUserComments.Designer.cs b/api/Migrations/20240626013021_AppUserComments.Designer.cs new file mode 100644 index 0000000..1e71139 --- /dev/null +++ b/api/Migrations/20240626013021_AppUserComments.Designer.cs @@ -0,0 +1,422 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using api.Data; + +#nullable disable + +namespace api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20240626013021_AppUserComments")] + partial class AppUserComments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = "295c9ab6-c8d5-472f-b490-ff1e5da87f6e", + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = "0ae4256b-8052-46c9-8ad6-f02f6c382065", + Name = "User", + NormalizedName = "USER" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("api.Models.AppUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("api.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StockId") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("StockId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("api.Models.Portfolio", b => + { + b.Property("AppUserId") + .HasColumnType("nvarchar(450)"); + + b.Property("StockId") + .HasColumnType("int"); + + b.HasKey("AppUserId", "StockId"); + + b.HasIndex("StockId"); + + b.ToTable("Portfolios"); + }); + + modelBuilder.Entity("api.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Industry") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastDiv") + .HasColumnType("decimal(18,2)"); + + b.Property("MarketCap") + .HasColumnType("bigint"); + + b.Property("Purchase") + .HasColumnType("decimal(18,2)"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("api.Models.Comment", b => + { + b.HasOne("api.Models.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId"); + + b.HasOne("api.Models.Stock", "Stock") + .WithMany("Comments") + .HasForeignKey("StockId"); + + b.Navigation("AppUser"); + + b.Navigation("Stock"); + }); + + modelBuilder.Entity("api.Models.Portfolio", b => + { + b.HasOne("api.Models.AppUser", "AppUser") + .WithMany("Portfolios") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api.Models.Stock", "Stock") + .WithMany("Portfolios") + .HasForeignKey("StockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Stock"); + }); + + modelBuilder.Entity("api.Models.AppUser", b => + { + b.Navigation("Portfolios"); + }); + + modelBuilder.Entity("api.Models.Stock", b => + { + b.Navigation("Comments"); + + b.Navigation("Portfolios"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/Migrations/20240626013021_AppUserComments.cs b/api/Migrations/20240626013021_AppUserComments.cs new file mode 100644 index 0000000..63fefbd --- /dev/null +++ b/api/Migrations/20240626013021_AppUserComments.cs @@ -0,0 +1,88 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace api.Migrations +{ + /// + public partial class AppUserComments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "146526b9-67b3-47b4-a864-999f0a051e76"); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "6d132618-0f7b-439e-8d87-74a61132da37"); + + migrationBuilder.AddColumn( + name: "AppUserId", + table: "Comments", + type: "nvarchar(450)", + nullable: true); + + migrationBuilder.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { "0ae4256b-8052-46c9-8ad6-f02f6c382065", null, "User", "USER" }, + { "295c9ab6-c8d5-472f-b490-ff1e5da87f6e", null, "Admin", "ADMIN" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Comments_AppUserId", + table: "Comments", + column: "AppUserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Comments_AspNetUsers_AppUserId", + table: "Comments", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Comments_AspNetUsers_AppUserId", + table: "Comments"); + + migrationBuilder.DropIndex( + name: "IX_Comments_AppUserId", + table: "Comments"); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "0ae4256b-8052-46c9-8ad6-f02f6c382065"); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "295c9ab6-c8d5-472f-b490-ff1e5da87f6e"); + + migrationBuilder.DropColumn( + name: "AppUserId", + table: "Comments"); + + migrationBuilder.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { "146526b9-67b3-47b4-a864-999f0a051e76", null, "Admin", "ADMIN" }, + { "6d132618-0f7b-439e-8d87-74a61132da37", null, "User", "USER" } + }); + } + } +} diff --git a/api/Migrations/AppDbContextModelSnapshot.cs b/api/Migrations/AppDbContextModelSnapshot.cs index f8867c8..ca937d5 100644 --- a/api/Migrations/AppDbContextModelSnapshot.cs +++ b/api/Migrations/AppDbContextModelSnapshot.cs @@ -51,13 +51,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = "146526b9-67b3-47b4-a864-999f0a051e76", + Id = "295c9ab6-c8d5-472f-b490-ff1e5da87f6e", Name = "Admin", NormalizedName = "ADMIN" }, new { - Id = "6d132618-0f7b-439e-8d87-74a61132da37", + Id = "0ae4256b-8052-46c9-8ad6-f02f6c382065", Name = "User", NormalizedName = "USER" }); @@ -242,6 +242,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("AppUserId") + .HasColumnType("nvarchar(450)"); + b.Property("Content") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -258,6 +261,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("AppUserId"); + b.HasIndex("StockId"); b.ToTable("Comments"); @@ -365,10 +370,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("api.Models.Comment", b => { + b.HasOne("api.Models.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId"); + b.HasOne("api.Models.Stock", "Stock") .WithMany("Comments") .HasForeignKey("StockId"); + b.Navigation("AppUser"); + b.Navigation("Stock"); }); diff --git a/api/Models/Comment.cs b/api/Models/Comment.cs index 0d31eb1..13f7a38 100644 --- a/api/Models/Comment.cs +++ b/api/Models/Comment.cs @@ -11,5 +11,7 @@ public class Comment public DateTime CreatedOn { get; set; } = DateTime.Now; public int? StockId { get; set; } public Stock? Stock { get; set; } + public string AppUserId { get; set; } = string.Empty; + public AppUser? AppUser { get; set; } } } \ No newline at end of file diff --git a/api/Repositories/CommentRepo.cs b/api/Repositories/CommentRepo.cs index 2911d4f..e5294d0 100644 --- a/api/Repositories/CommentRepo.cs +++ b/api/Repositories/CommentRepo.cs @@ -17,12 +17,13 @@ public CommentRepo(AppDbContext context) } public async Task> GetAllAsync() { - return await _context.Comments.ToListAsync(); + return await _context.Comments.Include(a => a.AppUser).ToListAsync(); } public async Task GetByIdAsync(int id) { - var commentModel = await _context.Comments.FindAsync(id); + var commentModel = await _context.Comments.Include(a => a.AppUser) + .FirstOrDefaultAsync(c => c.Id == id); if (commentModel is null) { diff --git a/api/Repositories/StockRepo.cs b/api/Repositories/StockRepo.cs index 3141036..015c33a 100644 --- a/api/Repositories/StockRepo.cs +++ b/api/Repositories/StockRepo.cs @@ -43,6 +43,7 @@ public async Task> GetAllAsync(QueryObject query) { var stocks = _context.Stocks .Include(c => c.Comments) + .ThenInclude(a => a.AppUser) .AsQueryable(); stocks = FilterStocks(stocks, query);