From 7ce4ceee62446d0ba743a92df1b73a682c4b361e Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 16 May 2024 18:55:39 -0400 Subject: first steps to extension api --- .../src/Endpoints/WebExtensionAdminEndpoint.cs | 93 ++++++++++++ back-end/src/Model/SimpleBookmarkContext.cs | 33 +++++ back-end/src/Model/WebExtensionAuth.cs | 50 +++++++ back-end/src/Model/WebHistory/WebHistoryEntry.cs | 46 ++++++ back-end/src/Model/WebHistory/WebHistoryStore.cs | 156 +++++++++++++++++++++ 5 files changed, 378 insertions(+) create mode 100644 back-end/src/Endpoints/WebExtensionAdminEndpoint.cs create mode 100644 back-end/src/Model/WebExtensionAuth.cs create mode 100644 back-end/src/Model/WebHistory/WebHistoryEntry.cs create mode 100644 back-end/src/Model/WebHistory/WebHistoryStore.cs diff --git a/back-end/src/Endpoints/WebExtensionAdminEndpoint.cs b/back-end/src/Endpoints/WebExtensionAdminEndpoint.cs new file mode 100644 index 0000000..672a9e1 --- /dev/null +++ b/back-end/src/Endpoints/WebExtensionAdminEndpoint.cs @@ -0,0 +1,93 @@ +// 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 . + +using System.Net; +using System.Threading.Tasks; + +using VNLib.Net.Http; +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using VNLib.Hashing.IdentityUtility; +using VNLib.Plugins; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Validation; + +/* + * This endpoint is used by web clients to manage their web extensions and control + * authorization and access to the extension. + */ + +namespace SimpleBookmark.Endpoints +{ + [ConfigurationName("extension_endpoint")] + internal sealed class WebExtensionAdminEndpoint : ProtectedWebEndpoint + { + public WebExtensionAdminEndpoint(PluginBase plugin, IConfigScope config) + { + string path = config.GetRequiredProperty("path", p => p.GetString()!); + InitPathAndLog(path, plugin.Log.CreateScope("Extension-Admin")); + + + } + + + protected override async ValueTask PutAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + if (webm.Assert(entity.Files.Count == 1, "Invalid number of files uploaded")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + FileUpload upload = entity.Files[0]; + + if(webm.Assert(upload.Length < 1000, "Key data is too large to be valid")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + using VnMemoryStream keyData = await BufferInputAsync(entity); + + //Parse the stream data as a json web key with some extra data + ReadOnlyJsonWebKey jwk = ReadOnlyJsonWebKey.FromUtf8Bytes(keyData.AsSpan()); + + + } + + private static async Task BufferInputAsync(HttpEntity entity) + { + FileUpload upload = entity.Files[0]; + + //Preallocate memory for the upload + VnMemoryStream mem = new(MemoryUtil.Shared, (nuint)upload.Length, false); + try + { + //Copy the entity file data into the memory stream + await upload.FileData.CopyToAsync(mem, 4096, MemoryUtil.Shared, entity.EventCancellation); + } + catch + { + mem.Dispose(); + throw; + } + + return mem; + } + } +} diff --git a/back-end/src/Model/SimpleBookmarkContext.cs b/back-end/src/Model/SimpleBookmarkContext.cs index f0e53b1..e801538 100644 --- a/back-end/src/Model/SimpleBookmarkContext.cs +++ b/back-end/src/Model/SimpleBookmarkContext.cs @@ -15,6 +15,8 @@ using Microsoft.EntityFrameworkCore; +using SimpleBookmark.Model.WebHistory; + using VNLib.Plugins.Extensions.Data; using VNLib.Plugins.Extensions.Loading.Sql; @@ -27,6 +29,10 @@ namespace SimpleBookmark.Model public DbSet Bookmarks { get; set; } + public DbSet WebHistory { get; set; } + + public DbSet AuthorizedExtensions { get; set; } + public SimpleBookmarkContext(DbContextOptions options) : base(options) { } @@ -50,6 +56,33 @@ namespace SimpleBookmark.Model table.WithColumn(p => p.Description); table.WithColumn(p => p.Tags); }); + + /* + * Define the coloumn mappings for the WebHistoryEntry table + */ + builder.DefineTable(nameof(WebHistory), table => + { + table.WithColumn(p => p.Id).AllowNull(false); + table.WithColumn(p => p.Created); + table.WithColumn(p => p.LastModified); + table.WithColumn(p => p.UserId).AllowNull(false); + table.WithColumn(p => p.Title); + table.WithColumn(p => p.Url).AllowNull(false); + }); + + /* + * Define the coloumn mappings for the authorized extensions table + */ + builder.DefineTable(nameof(AuthorizedExtensions), table => + { + table.WithColumn(p => p.Id).AllowNull(false); + table.WithColumn(p => p.Created); + table.WithColumn(p => p.LastModified); + table.WithColumn(p => p.UserId).AllowNull(false); + table.WithColumn(p => p.ExtensionId).AllowNull(false); + table.WithColumn(p => p.PublicKey).AllowNull(false); + table.WithColumn(p => p.AdditionalData); + }); } } diff --git a/back-end/src/Model/WebExtensionAuth.cs b/back-end/src/Model/WebExtensionAuth.cs new file mode 100644 index 0000000..7be6a60 --- /dev/null +++ b/back-end/src/Model/WebExtensionAuth.cs @@ -0,0 +1,50 @@ +// 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 . + +using System; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Data.Abstractions; + + +namespace SimpleBookmark.Model +{ + internal sealed class WebExtensionAuth : DbModelBase, IUserEntity + { + [Key] + [MaxLength(64)] + public override string Id { get; set; } + + public override DateTime Created { get; set; } + + public override DateTime LastModified { get; set; } + + [JsonIgnore] + [MaxLength(64)] + public string? UserId { get; set; } + + [MaxLength(200)] + public string? ExtensionId { get; set; } + + [MaxLength(1000)] + public string? PublicKey { get; set; } + + [JsonIgnore] + [MaxLength(2000)] + public byte[]? AdditionalData { get; set; } + } +} diff --git a/back-end/src/Model/WebHistory/WebHistoryEntry.cs b/back-end/src/Model/WebHistory/WebHistoryEntry.cs new file mode 100644 index 0000000..8bc3960 --- /dev/null +++ b/back-end/src/Model/WebHistory/WebHistoryEntry.cs @@ -0,0 +1,46 @@ +// 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 . + +using System; +using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; + +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Data.Abstractions; + +namespace SimpleBookmark.Model.WebHistory +{ + internal sealed class WebHistoryEntry : DbModelBase, IUserEntity + { + [Key] + [MaxLength(64)] + public override string Id { get; set; } + + public override DateTime Created { get; set; } + + public override DateTime LastModified { get; set; } + + [JsonIgnore] + [MaxLength(64)] + public string? UserId { get; set; } + + [MaxLength(200)] + public string? Title { get; set; } + + [MaxLength(300)] + public string? Url { get; set; } + + } +} diff --git a/back-end/src/Model/WebHistory/WebHistoryStore.cs b/back-end/src/Model/WebHistory/WebHistoryStore.cs new file mode 100644 index 0000000..230d874 --- /dev/null +++ b/back-end/src/Model/WebHistory/WebHistoryStore.cs @@ -0,0 +1,156 @@ +// 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 . + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Utils; +using VNLib.Utils.Extensions; +using VNLib.Plugins; +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Data.Abstractions; +using VNLib.Plugins.Extensions.Loading.Sql; +using VNLib.Plugins.Extensions.Data.Extensions; + +namespace SimpleBookmark.Model.WebHistory +{ + internal sealed class WebHistoryStore(PluginBase plugin) : DbStore + { + private readonly IAsyncLazy dbOptions = plugin.GetContextOptionsAsync(); + + /// + public override IDbQueryLookup QueryTable { get; } = new WebHistoryQueryTable(); + + /// + public override IDbContextHandle GetNewContext() => new SimpleBookmarkContext(dbOptions.Value); + + /// + public override string GetNewRecordId() => Guid.NewGuid().ToString("n"); + + public override void OnRecordUpdate(WebHistoryEntry newRecord, WebHistoryEntry existing) + { + //Update existing record + existing.Title = newRecord.Title; + existing.Url = newRecord.Url; + } + + public async Task UpsertMultipleAsync(string userId, WebHistoryEntry[] entries, CancellationToken cancellation) + { + DateTime now = DateTime.UtcNow; + + /* + * Update the required entry data before entering into the DB. + * This includes forcing userid, new event IDs, and updating + * created and last modified times to the current time. + */ + + entries.WithTime(now, true) + .WithUserId(userId) + .ForEach(e => e.Id = GetNewRecordId()); + + string[] urls = entries.Select(e => e.Url!) + .Distinct() + .ToArray(); + + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + + WebHistoryEntry[] existing = await context.Set() + .Where(b => b.UserId == userId) + .Where(b => urls.Contains(b.Url)) + .ToArrayAsync(cancellation); + + + //Update last modified time for existing entires + existing.ForEach(e => e.LastModified = now); + + context.Set() + .AddRange(entries.IntersectBy(existing, p => p.Url)); + } + + public Task DeleteAllForUserAsync(string userId, CancellationToken cancellation) + { + return DeleteAfterAsync(userId, DateTime.MaxValue, cancellation); + } + + public async Task DeleteAfterAsync(string userId, DateTime afterTime, CancellationToken cancellation) + { + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + + WebHistoryEntry[] entities = await context.Set() + .Where(b => b.UserId == userId) + .Where(b => b.LastModified > afterTime) + .ToArrayAsync(cancellation); + + context.RemoveRange(entities); + + await context.SaveAndCloseAsync(true, cancellation); + + return entities.Length; + } + + public async Task DeleteAllAsync(string userId, string[] eventIds, CancellationToken cancellation) + { + //Remove duplicates and empty strings + eventIds = eventIds.Where(e => !string.IsNullOrWhiteSpace(e)) + .Distinct() + .ToArray(); + + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + + WebHistoryEntry[] entities = await context.Set() + .Where(b => b.UserId == userId) + .Where(b => eventIds.Contains(b.Id)) + .ToArrayAsync(cancellation); + + context.RemoveRange(entities); + + await context.SaveAndCloseAsync(true, cancellation); + + return entities.Length; + } + + private sealed class WebHistoryQueryTable : IDbQueryLookup + { + public IQueryable GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints) + { + string userId = constraints[0]; + + return from b in context.Set() + where b.UserId == userId + orderby b.Created descending + select b; + } + + public IQueryable GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints) + { + string eventId = constraints[0]; + string userId = constraints[1]; + + return from b in context.Set() + where b.UserId == userId && b.Id == eventId + select b; + } + } + + } +} -- cgit