diff --git a/YABA.API/Controllers/BookmarksController.cs b/YABA.API/Controllers/BookmarksController.cs index 99b087e..4f1fa27 100644 --- a/YABA.API/Controllers/BookmarksController.cs +++ b/YABA.API/Controllers/BookmarksController.cs @@ -25,6 +25,8 @@ namespace YABA.API.Controllers [ProducesResponseType((int)HttpStatusCode.BadRequest)] public async Task Create([FromBody] CreateBookmarkRequestDTO request) { + if (!ModelState.IsValid) return BadRequest(ModelState); + var result = await _bookmarkService.CreateBookmark(request); if(!result.IsSuccessful) return BadRequest(); @@ -103,12 +105,12 @@ namespace YABA.API.Controllers return Ok(new GenericResponse(result)); } - [HttpDelete()] + [HttpDelete] [ProducesResponseType(typeof(IEnumerable>), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] - public async Task DeleteBookmarks(IEnumerable ids) + public async Task DeleteBookmarks([FromBody] DeleteBookmarksRequest request) { - var result = await _bookmarkService.DeleteBookmarks(ids); + var result = await _bookmarkService.DeleteBookmarks(request.Ids); if(result.All(x => !x.IsSuccessful)) return NotFound(); diff --git a/YABA.API/ViewModels/DeleteBookmarksRequest.cs b/YABA.API/ViewModels/DeleteBookmarksRequest.cs new file mode 100644 index 0000000..bf031e2 --- /dev/null +++ b/YABA.API/ViewModels/DeleteBookmarksRequest.cs @@ -0,0 +1,7 @@ +namespace YABA.API.ViewModels +{ + public class DeleteBookmarksRequest + { + public IEnumerable Ids { get; set; } + } +} diff --git a/YABA.Common/DTOs/Bookmarks/BookmarkDTO.cs b/YABA.Common/DTOs/Bookmarks/BookmarkDTO.cs index cc6171e..d0d52a3 100644 --- a/YABA.Common/DTOs/Bookmarks/BookmarkDTO.cs +++ b/YABA.Common/DTOs/Bookmarks/BookmarkDTO.cs @@ -11,11 +11,10 @@ namespace YABA.Common.DTOs.Bookmarks public DateTimeOffset CreatedOn { get; set; } public DateTimeOffset LastModified { get; set; } public string Title { get; set; } - public string Description { get; set; } - public string Note { get; set; } + public string? Description { get; set; } + public string? Note { get; set; } public bool IsHidden { get; set; } public string Url { get; set; } public IList Tags { get; set; } = new List(); - } } diff --git a/YABA.Common/DTOs/Bookmarks/CreateBookmarkRequestDTO.cs b/YABA.Common/DTOs/Bookmarks/CreateBookmarkRequestDTO.cs index 148a378..92823c3 100644 --- a/YABA.Common/DTOs/Bookmarks/CreateBookmarkRequestDTO.cs +++ b/YABA.Common/DTOs/Bookmarks/CreateBookmarkRequestDTO.cs @@ -1,13 +1,16 @@ -using YABA.Common.Interfaces; +using System.ComponentModel.DataAnnotations; +using YABA.Common.Interfaces; namespace YABA.Common.DTOs.Bookmarks { public class CreateBookmarkRequestDTO : IBookmark { - public string Title { get; set; } - public string Description { get; set; } - public string Note { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public string? Note { get; set; } public bool IsHidden { get; set; } + + [Required] public string Url { get; set; } } } diff --git a/YABA.Common/DTOs/Bookmarks/UpdateBookmarkRequestDTO.cs b/YABA.Common/DTOs/Bookmarks/UpdateBookmarkRequestDTO.cs index cc4dbce..5fceba3 100644 --- a/YABA.Common/DTOs/Bookmarks/UpdateBookmarkRequestDTO.cs +++ b/YABA.Common/DTOs/Bookmarks/UpdateBookmarkRequestDTO.cs @@ -1,12 +1,16 @@ -using YABA.Common.Interfaces; +using System.ComponentModel.DataAnnotations; +using YABA.Common.Interfaces; namespace YABA.Common.DTOs.Bookmarks { public class UpdateBookmarkRequestDTO : IBookmark { - public string Title { get; set; } - public string Description { get; set; } - public string Note { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public string? Note { get; set; } public bool IsHidden { get; set; } + + [Required] + public string Url { get; set; } } } diff --git a/YABA.Common/Interfaces/IBookmark.cs b/YABA.Common/Interfaces/IBookmark.cs index 7ae2aed..0fe0c06 100644 --- a/YABA.Common/Interfaces/IBookmark.cs +++ b/YABA.Common/Interfaces/IBookmark.cs @@ -4,8 +4,9 @@ namespace YABA.Common.Interfaces public interface IBookmark { public string Title { get; set; } - public string Description { get; set; } - public string Note { get; set; } + public string? Description { get; set; } + public string? Note { get; set; } public bool IsHidden { get; set; } + public string Url { get; set; } } } diff --git a/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.Designer.cs b/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.Designer.cs new file mode 100644 index 0000000..cbeec1c --- /dev/null +++ b/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.Designer.cs @@ -0,0 +1,210 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using YABA.Data.Context; + +namespace YABA.Data.Migrations +{ + [DbContext(typeof(YABABaseContext))] + [Migration("20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional")] + partial class ModifiedBookmarkTable_MakeNoteAndDescriptionOptional + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.17") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("YABA.Models.Bookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("Note") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text") + .HasColumnName("url"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_bookmarks"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_bookmarks_user_id"); + + b.ToTable("bookmarks"); + }); + + modelBuilder.Entity("YABA.Models.BookmarkTag", b => + { + b.Property("BookmarkId") + .HasColumnType("integer") + .HasColumnName("bookmark_id"); + + b.Property("TagId") + .HasColumnType("integer") + .HasColumnName("tag_id"); + + b.HasKey("BookmarkId", "TagId") + .HasName("pk_bookmark_tags"); + + b.HasIndex("TagId") + .HasDatabaseName("ix_bookmark_tags_tag_id"); + + b.ToTable("bookmark_tags"); + }); + + modelBuilder.Entity("YABA.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_tags_name"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tags_user_id"); + + b.ToTable("tags"); + }); + + modelBuilder.Entity("YABA.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Auth0Id") + .IsRequired() + .HasColumnType("text") + .HasColumnName("auth0id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Auth0Id") + .IsUnique() + .HasDatabaseName("ix_users_auth0id"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("YABA.Models.Bookmark", b => + { + b.HasOne("YABA.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_bookmarks_users_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("YABA.Models.BookmarkTag", b => + { + b.HasOne("YABA.Models.Bookmark", "Bookmark") + .WithMany() + .HasForeignKey("BookmarkId") + .HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YABA.Models.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .HasConstraintName("fk_bookmark_tags_tags_tag_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bookmark"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("YABA.Models.Tag", b => + { + b.HasOne("YABA.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_tags_users_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.cs b/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.cs new file mode 100644 index 0000000..21f55ca --- /dev/null +++ b/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace YABA.Data.Migrations +{ + public partial class ModifiedBookmarkTable_MakeNoteAndDescriptionOptional : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "note", + table: "bookmarks", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "description", + table: "bookmarks", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "note", + table: "bookmarks", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "description", + table: "bookmarks", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + } +} diff --git a/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs b/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs index 49f0d9b..8de4ae5 100644 --- a/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs +++ b/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs @@ -32,7 +32,6 @@ namespace YABA.Data.Migrations .HasColumnName("created_on"); b.Property("Description") - .IsRequired() .HasColumnType("text") .HasColumnName("description"); @@ -45,7 +44,6 @@ namespace YABA.Data.Migrations .HasColumnName("last_modified"); b.Property("Note") - .IsRequired() .HasColumnType("text") .HasColumnName("note"); diff --git a/YABA.Models/Bookmark.cs b/YABA.Models/Bookmark.cs index 5a7dacf..86e7f92 100644 --- a/YABA.Models/Bookmark.cs +++ b/YABA.Models/Bookmark.cs @@ -12,8 +12,8 @@ namespace YABA.Models public DateTimeOffset CreatedOn { get; set; } public DateTimeOffset LastModified { get; set; } public string Title { get; set; } - public string Description { get; set; } - public string Note { get; set; } + public string? Description { get; set; } + public string? Note { get; set; } public bool IsHidden { get; set; } public string Url { get; set; } diff --git a/YABA.Service/BookmarkService.cs b/YABA.Service/BookmarkService.cs index e156fa0..56484bb 100644 --- a/YABA.Service/BookmarkService.cs +++ b/YABA.Service/BookmarkService.cs @@ -1,8 +1,11 @@ using AutoMapper; +using HtmlAgilityPack; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Text.RegularExpressions; using System.Threading.Tasks; using YABA.Common.DTOs; using YABA.Common.DTOs.Bookmarks; @@ -84,6 +87,7 @@ namespace YABA.Service } var bookmark = _mapper.Map(request); + UpdateBookmarkWithMetaData(bookmark); bookmark.UserId = currentUserId; await _context.Bookmarks.AddAsync(bookmark); @@ -106,6 +110,8 @@ namespace YABA.Service bookmark.Description = request.Description; bookmark.Note = request.Note; bookmark.IsHidden = request.IsHidden; + bookmark.Url = request.Url; + UpdateBookmarkWithMetaData(bookmark); if (await _context.SaveChangesAsync() > 0) crudResult.CrudResult = CrudResultLookup.UpdateSucceeded; @@ -216,5 +222,32 @@ namespace YABA.Service int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId); return userId; } + + private void UpdateBookmarkWithMetaData(IBookmark bookmark) + { + var webClient = new WebClient(); + var sourceData = webClient.DownloadString(bookmark.Url); + var title = Regex.Match(sourceData, @"\]*\>\s*(?[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value; + var description = string.Empty; + + var getHtmlDoc = new HtmlWeb(); + var document = getHtmlDoc.Load(bookmark.Url); + var metaTags = document.DocumentNode.SelectNodes("//meta"); + if (metaTags != null) + { + foreach (var sitetag in metaTags) + { + if (sitetag.Attributes["name"] != null && sitetag.Attributes["content"] != null && sitetag.Attributes["name"].Value == "description") + + { + + description = sitetag.Attributes["content"].Value; + } + } + } + + bookmark.Title = !string.IsNullOrEmpty(bookmark.Title) ? bookmark.Title : string.IsNullOrEmpty(title) ? bookmark.Url : title; + bookmark.Description = !string.IsNullOrEmpty(bookmark.Description) ? bookmark.Description : description; + } } } diff --git a/YABA.Service/YABA.Service.csproj b/YABA.Service/YABA.Service.csproj index 846cf6b..1dedb6e 100644 --- a/YABA.Service/YABA.Service.csproj +++ b/YABA.Service/YABA.Service.csproj @@ -7,6 +7,7 @@ <ItemGroup> <PackageReference Include="AutoMapper" Version="12.0.1" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.46" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />