From b0cb791bc2acb14a697bc1304049afdd00fc7b71 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Thu, 2 Mar 2023 23:45:57 -0600 Subject: [PATCH 1/2] Add auto-registration mechanism - Add User registration mechanism when adding auth provider id to claims, if no User Id corresponds to the auth provider Id - Swallow unique constraint violation for unique auth provider id on user table, in case of duplicate requests - Add Serilog logging - Add no bookmarks and no tags message when none is found on Bookmark List page --- .gitignore | 3 +- YABA.API/Controllers/MiscController.cs | 3 +- YABA.API/Controllers/TestController.cs | 42 ++++++++++++ .../Middlewares/AddCustomClaimsMiddleware.cs | 7 ++ YABA.API/Program.cs | 5 ++ YABA.API/Settings/DevOnlyAttribute.cs | 34 ++++++++++ YABA.API/YABA.API.csproj | 4 ++ YABA.API/appsettings.json | 12 ++++ YABA.Service/UserService.cs | 47 +++++++++++--- yaba-web/src/views/bookmarksListView.jsx | 64 +++++++++---------- 10 files changed, 177 insertions(+), 44 deletions(-) create mode 100644 YABA.API/Controllers/TestController.cs create mode 100644 YABA.API/Settings/DevOnlyAttribute.cs diff --git a/.gitignore b/.gitignore index cd8907d..e73ac58 100644 --- a/.gitignore +++ b/.gitignore @@ -246,4 +246,5 @@ logs/ npm-debug.log* yarn-debug.log* yarn-error.log* -.env \ No newline at end of file +.env +Logs/* \ No newline at end of file diff --git a/YABA.API/Controllers/MiscController.cs b/YABA.API/Controllers/MiscController.cs index e497a6f..09cbf6d 100644 --- a/YABA.API/Controllers/MiscController.cs +++ b/YABA.API/Controllers/MiscController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Net; +using YABA.API.Settings; using YABA.API.ViewModels; using YABA.Service.Interfaces; @@ -24,7 +25,7 @@ namespace YABA.API.Controllers } [HttpGet] - [Obsolete] + [DevOnly] [Route("GetWebsiteMetaData")] [ProducesResponseType(typeof(GetWebsiteMetaDataResponse), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] diff --git a/YABA.API/Controllers/TestController.cs b/YABA.API/Controllers/TestController.cs new file mode 100644 index 0000000..78ade0d --- /dev/null +++ b/YABA.API/Controllers/TestController.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using System.Net; +using YABA.API.Settings; + +namespace YABA.API.Controllers +{ + [ApiController] + [ApiVersion("1")] + [DevOnly] + [Route("api/v{version:apiVersion}/[controller]")] + public class TestController : ControllerBase + { + private readonly ILogger _logger; + + public TestController(ILogger logger) + { + _logger = logger; + } + + [HttpGet("TestLog")] + [ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + public IActionResult TestLog() + { + var testObject = new { id = 1, name = "Test Message" }; + _logger.LogDebug("Testing debug: {@TestObject}", testObject); + return Ok(testObject); + } + + [HttpGet("TestLogError")] + [ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + public IActionResult TestLogError() + { + var testObject = new { id = 1, name = "Test Message" }; + throw new Exception(); + return Ok(testObject); + } + } +} diff --git a/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs b/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs index db3893f..14ef26e 100644 --- a/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs +++ b/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs @@ -24,6 +24,13 @@ namespace YABA.API.Middlewares if (!string.IsNullOrEmpty(userAuthProviderId)) { var userId = userService.GetUserId(userAuthProviderId); + + if(userId <= 0) + { + var registedUser = await userService.RegisterUser(userAuthProviderId); + userId = registedUser.Id; + } + httpContext.User.Identities.FirstOrDefault().AddClaim(new Claim(ClaimsLookup.UserId.GetClaimName(), userId.ToString())); } } diff --git a/YABA.API/Program.cs b/YABA.API/Program.cs index b9ae8be..bcfce83 100644 --- a/YABA.API/Program.cs +++ b/YABA.API/Program.cs @@ -12,6 +12,7 @@ using YABA.API.Settings.Swashbuckle; using YABA.Data.Configuration; using YABA.Data.Context; using YABA.Service.Configuration; +using Serilog; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; @@ -77,6 +78,10 @@ builder.Services.AddSwaggerGen( } ); +// Add Serilog +Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger(); +builder.Host.UseSerilog(); + var app = builder.Build(); // Run database migrations diff --git a/YABA.API/Settings/DevOnlyAttribute.cs b/YABA.API/Settings/DevOnlyAttribute.cs new file mode 100644 index 0000000..c3886d0 --- /dev/null +++ b/YABA.API/Settings/DevOnlyAttribute.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace YABA.API.Settings +{ + // Source: https://stackoverflow.com/questions/56495475/asp-net-core-its-possible-to-configure-an-action-in-controller-only-in-developm + public class DevOnlyAttribute : Attribute, IFilterFactory + { + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return new DevOnlyAttributeImpl(serviceProvider.GetRequiredService()); + } + + public bool IsReusable => true; + + private class DevOnlyAttributeImpl : Attribute, IAuthorizationFilter + { + public DevOnlyAttributeImpl(IWebHostEnvironment hostingEnv) + { + HostingEnv = hostingEnv; + } + + private IWebHostEnvironment HostingEnv { get; } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!HostingEnv.IsDevelopment()) + { + context.Result = new NotFoundResult(); + } + } + } + } +} diff --git a/YABA.API/YABA.API.csproj b/YABA.API/YABA.API.csproj index ed1d9b0..7cb397c 100644 --- a/YABA.API/YABA.API.csproj +++ b/YABA.API/YABA.API.csproj @@ -18,6 +18,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/YABA.API/appsettings.json b/YABA.API/appsettings.json index 117c848..97d4479 100644 --- a/YABA.API/appsettings.json +++ b/YABA.API/appsettings.json @@ -6,6 +6,18 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + "Serilog": { + "MinimumLevel": "Information", + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "Logs/log-.txt", + "rollingInterval": "Day" + } + } + ] + }, "WebClient": { "Url": "https://localhost:3000" } diff --git a/YABA.Service/UserService.cs b/YABA.Service/UserService.cs index 534cd52..f76f7e9 100644 --- a/YABA.Service/UserService.cs +++ b/YABA.Service/UserService.cs @@ -1,4 +1,8 @@ using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Npgsql; +using System; using System.Linq; using System.Threading.Tasks; using YABA.Common.DTOs; @@ -13,12 +17,14 @@ namespace YABA.Service private readonly YABAReadOnlyContext _roContext; private readonly YABAReadWriteContext _context; private readonly IMapper _mapper; + private readonly ILogger _logger; - public UserService (YABAReadOnlyContext roContext, YABAReadWriteContext context, IMapper mapper) + public UserService (YABAReadOnlyContext roContext, YABAReadWriteContext context, IMapper mapper, ILogger logger) { _roContext = roContext; _context = context; _mapper = mapper; + _logger = logger; } public bool IsUserRegistered(string authProviderId) @@ -28,19 +34,40 @@ namespace YABA.Service public async Task RegisterUser(string authProviderId) { - if(IsUserRegistered(authProviderId)) + try { - var user = _roContext.Users.FirstOrDefault(x => x.Auth0Id == authProviderId); - return _mapper.Map(user); + if (!IsUserRegistered(authProviderId)) + { + var userToRegister = new User + { + Auth0Id = authProviderId + }; + + var registedUser = _context.Users.Add(userToRegister); + await _context.SaveChangesAsync(); + } + } + catch (Exception ex) + { + if(ex.InnerException is PostgresException && + ((PostgresException)ex.InnerException).Code == "23505") + { + var postgresException = (PostgresException)ex.InnerException; + _logger.LogWarning("Swallowing constraint violation: {@ConstraintName} for {@AuthProviderId}", postgresException.ConstraintName, authProviderId); + } + else + { + throw ex; + } } - var userToRegister = new User - { - Auth0Id = authProviderId - }; + return await Get(authProviderId); + } - var registedUser = _context.Users.Add(userToRegister); - return await _context.SaveChangesAsync() > 0 ? _mapper.Map(registedUser.Entity) : null; + public async Task Get(string authProviderId) + { + var user = await _roContext.Users.FirstOrDefaultAsync(x => x.Auth0Id == authProviderId); + return _mapper.Map(user); } public int GetUserId(string authProviderId) diff --git a/yaba-web/src/views/bookmarksListView.jsx b/yaba-web/src/views/bookmarksListView.jsx index 18afe94..2d08a20 100644 --- a/yaba-web/src/views/bookmarksListView.jsx +++ b/yaba-web/src/views/bookmarksListView.jsx @@ -276,45 +276,44 @@ export function BookmarksListView(props) {
- - -
+ { + (bookmarksState.length > 0 || getSelectedBookmarksCount() > 0) && + { - getDisplayedBookmarksCount() <= 0 && - No bookmarks to display + bookmarksState.length > 0 && + } - - { - getDisplayedBookmarksCount() > 0 && - - } - + + { getSelectedBookmarksCount() > 0 && - - handleHideBookmarks(getSelectedBookmarks().map(x => x.id))}> - {props.showHidden ? "Unhide" : "Hide"} - - - Delete - - +
+ {getSelectedBookmarksCount()} selected + + +
} - -
- - - { - getSelectedTags().length > 0 && ( - getSelectedTags().map((tag) => { - return - }) - ) - } - -
+ + + { + getSelectedTags().length > 0 && ( + getSelectedTags().map((tag) => { + return + }) + ) + } + + + } + + { getFilteredBookmarks().length <= 0 &&
No Bookmarks found
} { getFilteredBookmarks().map((bookmark) => { return + { getTagGroups(getNotSelectedTags()).length <= 0 &&
No Tags gound
} { getTagGroups(getNotSelectedTags()).map((group) => { return
-- 2.49.0 From 34d459cc586df83387a6a07e5f8a0b0b0d85d48b Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Sat, 4 Mar 2023 00:37:59 -0600 Subject: [PATCH 2/2] Fixed 403 Forbidden thrown when getting Website metadata - Add User-Agent to header to prevent 403 Forbidden when downloading webpage strings - Add Bookmark URL and User Id unique combo constraint - Add Tag Name and User Id unique combo constraint --- YABA.Data/Context/YABABaseContext.cs | 6 +- ...serIdComboConstraintToBookmark.Designer.cs | 214 ++++++++++++++++++ ...ueUrlAndUserIdComboConstraintToBookmark.cs | 43 ++++ .../YABABaseContextModelSnapshot.cs | 12 +- YABA.Service/BookmarkService.cs | 1 + 5 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 YABA.Data/Migrations/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.Designer.cs create mode 100644 YABA.Data/Migrations/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.cs diff --git a/YABA.Data/Context/YABABaseContext.cs b/YABA.Data/Context/YABABaseContext.cs index 33237ca..13fcdb7 100644 --- a/YABA.Data/Context/YABABaseContext.cs +++ b/YABA.Data/Context/YABABaseContext.cs @@ -39,7 +39,11 @@ namespace YABA.Data.Context .IsUnique(); modelBuilder.Entity() - .HasIndex(x => x.Name) + .HasIndex(x => new { x.Name, x.UserId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(x => new { x.Url, x.UserId }) .IsUnique(); } diff --git a/YABA.Data/Migrations/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.Designer.cs b/YABA.Data/Migrations/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.Designer.cs new file mode 100644 index 0000000..a195395 --- /dev/null +++ b/YABA.Data/Migrations/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.Designer.cs @@ -0,0 +1,214 @@ +// +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("20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark")] + partial class AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark + { + 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.HasIndex("Url", "UserId") + .IsUnique() + .HasDatabaseName("ix_bookmarks_url_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("UserId") + .HasDatabaseName("ix_tags_user_id"); + + b.HasIndex("Name", "UserId") + .IsUnique() + .HasDatabaseName("ix_tags_name_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/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.cs b/YABA.Data/Migrations/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.cs new file mode 100644 index 0000000..f712c27 --- /dev/null +++ b/YABA.Data/Migrations/20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace YABA.Data.Migrations +{ + public partial class AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_tags_name", + table: "tags"); + + migrationBuilder.CreateIndex( + name: "ix_tags_name_user_id", + table: "tags", + columns: new[] { "name", "user_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_bookmarks_url_user_id", + table: "bookmarks", + columns: new[] { "url", "user_id" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_tags_name_user_id", + table: "tags"); + + migrationBuilder.DropIndex( + name: "ix_bookmarks_url_user_id", + table: "bookmarks"); + + migrationBuilder.CreateIndex( + name: "ix_tags_name", + table: "tags", + column: "name", + unique: true); + } + } +} diff --git a/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs b/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs index 8de4ae5..dd6ba10 100644 --- a/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs +++ b/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs @@ -67,6 +67,10 @@ namespace YABA.Data.Migrations b.HasIndex("UserId") .HasDatabaseName("ix_bookmarks_user_id"); + b.HasIndex("Url", "UserId") + .IsUnique() + .HasDatabaseName("ix_bookmarks_url_user_id"); + b.ToTable("bookmarks"); }); @@ -113,13 +117,13 @@ namespace YABA.Data.Migrations b.HasKey("Id") .HasName("pk_tags"); - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("ix_tags_name"); - b.HasIndex("UserId") .HasDatabaseName("ix_tags_user_id"); + b.HasIndex("Name", "UserId") + .IsUnique() + .HasDatabaseName("ix_tags_name_user_id"); + b.ToTable("tags"); }); diff --git a/YABA.Service/BookmarkService.cs b/YABA.Service/BookmarkService.cs index 8fc333a..a4ec7af 100644 --- a/YABA.Service/BookmarkService.cs +++ b/YABA.Service/BookmarkService.cs @@ -253,6 +253,7 @@ namespace YABA.Service private void UpdateBookmarkWithMetaData(IBookmark bookmark) { var webClient = new WebClient(); + webClient.Headers.Add("User-Agent", "APIClient"); var sourceData = webClient.DownloadString(bookmark.Url); var title = Regex.Match(sourceData, @"\]*\>\s*(?[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value; var description = string.Empty; -- 2.49.0