diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-20 23:49:29 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-20 23:49:29 -0500 |
commit | 6cb7da37824d02a1898d08d0f9495c77fde4dd1d (patch) | |
tree | 95e37ea3c20f416d6a205ee4ab050c307b18eafe /back-end/src/Endpoints |
inital commit
Diffstat (limited to 'back-end/src/Endpoints')
-rw-r--r-- | back-end/src/Endpoints/BookmarkEndpoint.cs | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/back-end/src/Endpoints/BookmarkEndpoint.cs b/back-end/src/Endpoints/BookmarkEndpoint.cs new file mode 100644 index 0000000..b7825d6 --- /dev/null +++ b/back-end/src/Endpoints/BookmarkEndpoint.cs @@ -0,0 +1,373 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; +using System.Net; +using System.Linq; +using System.Buffers; +using System.Text.Json; +using System.Collections; +using SimpleBookmark.Model; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +using FluentValidation; +using FluentValidation.Results; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Utils; +using VNLib.Utils.Logging; +using VNLib.Plugins; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Sql; +using VNLib.Plugins.Extensions.Data.Extensions; +using VNLib.Plugins.Extensions.Validation; + +namespace SimpleBookmark.Endpoints +{ + + [ConfigurationName("bm_endpoint")] + internal sealed class BookmarkEndpoint : ProtectedWebEndpoint + { + private static readonly IValidator<BookmarkEntry> BmValidator = BookmarkEntry.GetValidator(); + + private readonly BookmarkStore Bookmarks; + private readonly BookmarkStoreConfig BmConfig; + + public BookmarkEndpoint(PluginBase plugin, IConfigScope config) + { + string? path = config.GetRequiredProperty("path", p => p.GetString()!); + InitPathAndLog(path, plugin.Log); + + //Init new bookmark store + IAsyncLazy<DbContextOptions> options = plugin.GetContextOptionsAsync(); + Bookmarks = new BookmarkStore(options); + + //Load config + BmConfig = config.GetRequiredProperty("config", p => p.Deserialize<BookmarkStoreConfig>()!); + } + + ///<inheritdoc/> + protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) + { + if (!entity.Session.CanRead()) + { + WebMessage webm = new() + { + Result = "You do not have permissions to read records", + Success = false + }; + + return VirtualClose(entity, webm, HttpStatusCode.Forbidden); + } + + if (entity.QueryArgs.TryGetNonEmptyValue("id", out string? singleId)) + { + //Try to get single record for the current user + BookmarkEntry? single = await Bookmarks.GetSingleUserRecordAsync(singleId, entity.Session.UserID); + + //Return result + return single is null ? VfReturnType.NotFound : VirtualOkJson(entity, single); + } + + if (entity.QueryArgs.ContainsKey("getTags")) + { + //Try to get all tags for the current user + string[] allTags = await Bookmarks.GetAllTagsForUserAsync(entity.Session.UserID, entity.EventCancellation); + + //Return result + return VirtualOkJson(entity, allTags); + } + + //See if count query + if(entity.QueryArgs.TryGetNonEmptyValue("count", out string? countS)) + { + //Try to get count + long count = await Bookmarks.GetUserRecordCountAsync(entity.Session.UserID, entity.EventCancellation); + + WebMessage webm = new () + { + Result = count, + Success = true + }; + + //Return result + return VirtualOk(entity, webm); + } + + //Get query parameters + _ = entity.QueryArgs.TryGetNonEmptyValue("limit", out string? limitS); + _ = uint.TryParse(limitS, out uint limit); + //Clamp limit to max limit + limit = Math.Clamp(limit, BmConfig.DefaultLimit, BmConfig.MaxLimit); + + //try to parse offset + _ = entity.QueryArgs.TryGetNonEmptyValue("page", out string? offsetS); + _ = uint.TryParse(offsetS, out uint offset); + + //Get any query arguments + if(entity.QueryArgs.TryGetNonEmptyValue("q", out string? query)) + { + //Replace percent encoding with spaces + query = query.Replace('+', ' '); + } + + string[] tags = Array.Empty<string>(); + + //Get tags + if (entity.QueryArgs.TryGetNonEmptyValue("t", out string? tagsS)) + { + //Split tags at spaces and remove empty entries + tags = tagsS.Split('+') + .Where(static t => !string.IsNullOrWhiteSpace(t)) + .ToArray(); + } + + //Get bookmarks + BookmarkEntry[] bookmarks = await Bookmarks.SearchBookmarksAsync( + entity.Session.UserID, + query, + tags, + (int)limit, + (int)offset, + entity.EventCancellation + ); + + //Return result + return VirtualOkJson(entity, bookmarks); + } + + ///<inheritdoc/> + protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + if (webm.Assert(entity.Session.CanWrite(), "You do not have permissions to create records")) + { + return VirtualClose(entity, webm, HttpStatusCode.Forbidden); + } + + //try to get the update from the request body + BookmarkEntry? newBookmark = await entity.GetJsonFromFileAsync<BookmarkEntry>(); + + if (webm.Assert(newBookmark != null, "No data was provided")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + //Remove any user id from the update + newBookmark.UserId = null; + + if (!BmValidator.Validate(newBookmark, webm)) + { + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); + } + + //See if the uses has reached their quota + long count = await Bookmarks.GetUserRecordCountAsync(entity.Session.UserID, entity.EventCancellation); + + if(webm.Assert(count <= BmConfig.PerPersonQuota, "You have reached your bookmark quota")) + { + return VirtualClose(entity, webm, HttpStatusCode.OK); + } + + //Try to create the record + ERRNO result = await Bookmarks.CreateUserRecordAsync(newBookmark, entity.Session.UserID, entity.EventCancellation); + + if (webm.Assert(result > 0, "Failed to create new bookmark")) + { + return VirtualClose(entity, webm, HttpStatusCode.OK); + } + + webm.Result = "Successfully created bookmark"; + webm.Success = true; + + return VirtualClose(entity, webm, HttpStatusCode.Created); + } + + ///<inheritdoc/> + protected override async ValueTask<VfReturnType> PatchAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + if (webm.Assert(entity.Session.CanWrite(), "You do not have permissions to update records")) + { + return VirtualClose(entity, webm, HttpStatusCode.Forbidden); + } + + //try to get the update from the request body + BookmarkEntry? update = await entity.GetJsonFromFileAsync<BookmarkEntry>(); + + if (webm.Assert(update != null, "No data was provided")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + if (webm.Assert(update!.Id != null, "The bookmark object is malformatted for this request")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + //Remove any user id from the update + update.UserId = null; + + if (!BmValidator.Validate(update, webm)) + { + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); + } + + //Try to update the record + ERRNO result = await Bookmarks.UpdateUserRecordAsync(update, entity.Session.UserID, entity.EventCancellation); + + if (webm.Assert(result > 0, "Failed to update existing record")) + { + return VirtualClose(entity, webm, HttpStatusCode.NotFound); + } + + webm.Result = "Successfully updated bookmark"; + webm.Success = true; + + return VirtualClose(entity, webm, HttpStatusCode.OK); + } + + /* + * PUT method is only used for bulk uploads + */ + + ///<inheritdoc/> + protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + if (webm.Assert(entity.Session.CanWrite(), "You do not have permissions to update records")) + { + return VirtualClose(entity, webm, HttpStatusCode.Forbidden); + } + + //See if the user wants to fail on invalid records + bool failOnInvalid = entity.QueryArgs.ContainsKey("failOnInvalid"); + + //try to get the update from the request body + BookmarkEntry[]? batch = await entity.GetJsonFromFileAsync<BookmarkEntry[]>(); + + if (webm.Assert(batch != null, "No data was provided")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + //filter out any null entries + IEnumerable<BookmarkEntry> sanitized = batch.Where(static b => b != null); + + if (failOnInvalid) + { + //Get any invalid entires and create a validation result + BookmarkError[] invalidEntires = sanitized.Select(b => + { + ValidationResult result = BmValidator.Validate(b); + if(result.IsValid) + { + return null; + } + + return new BookmarkError() + { + Errors = result.GetErrorsAsCollection(), + Subject = b + }; + + }) + .Where(static b => b != null) + .ToArray()!; + + //At least one error + if(invalidEntires.Length > 0) + { + //Notify the user of the invalid entires + BatchUploadResult res = new() + { + Errors = invalidEntires + }; + + webm.Result = res; + return VirtualOk(entity, webm); + } + } + else + { + //Remove any invalid entires + sanitized = sanitized.Where(static b => BmValidator.Validate(b).IsValid); + } + + //Try to update the records + ERRNO result = await Bookmarks.AddBulkAsync(sanitized, entity.Session.UserID, false, entity.EventCancellation); + + webm.Result = $"Successfully added {result} of {batch.Length} bookmarks"; + webm.Success = true; + + return VirtualClose(entity, webm, HttpStatusCode.OK); + } + + ///<inheritdoc/> + protected override async ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + if (webm.Assert(entity.Session.CanDelete(), "You do not have permissions to delete records")) + { + return VirtualClose(entity, webm, HttpStatusCode.Forbidden); + } + + if(!entity.QueryArgs.TryGetNonEmptyValue("id", out string? deleteId)) + { + webm.Result = "No id was provided"; + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + //Try to delete the record + ERRNO result = await Bookmarks.DeleteUserRecordAsync(deleteId, entity.Session.UserID); + + if (webm.Assert(result > 0, "Requested bookmark does not exist")) + { + return VirtualClose(entity, webm, HttpStatusCode.NotFound); + } + + webm.Result = "Successfully deleted bookmark"; + webm.Success = true; + + return VirtualClose(entity, webm, HttpStatusCode.OK); + } + + sealed class BatchUploadResult + { + [JsonPropertyName("invalid")] + public BookmarkError[]? Errors { get; set; } + } + + sealed class BookmarkError + { + [JsonPropertyName("errors")] + public ICollection? Errors { get; set; } + + [JsonPropertyName("subject")] + public BookmarkEntry? Subject { get; set; } + } + } +} |