diff options
author | vnugent <public@vaughnnugent.com> | 2024-03-13 16:19:50 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-03-13 16:19:50 -0400 |
commit | e326736021be8ff5af4208d16f59d5e3e4f22b3e (patch) | |
tree | c6ed7dfefd5b9c8965cfc185ad3f89436301df1a | |
parent | 3883de080e263d2f076f65b4600a5021d3d64a21 (diff) |
Squashed commit of the following:v0.1.1
commit 1e08c6d2112459dc02a0ab873123c4a363b01d21
Author: vnugent <public@vaughnnugent.com>
Date: Wed Mar 13 16:17:58 2024 -0400
ci: verified container build ready for next release
commit 85a1e5b7cc5c99e97a2d4e99bbceb0d2139742ff
Author: vnugent <public@vaughnnugent.com>
Date: Tue Mar 12 22:05:16 2024 -0400
ci: exciting bare-metal build process, os support, smaller packages
commit 748cdbf4880d830fd794e92856e8c35a46e4f884
Author: vnugent <public@vaughnnugent.com>
Date: Mon Mar 11 21:21:18 2024 -0400
feat(app): #1 update libs & add curl support
33 files changed, 1322 insertions, 431 deletions
diff --git a/back-end/src/Endpoints/SiteLookupEndpoint.cs b/back-end/src/Endpoints/SiteLookupEndpoint.cs new file mode 100644 index 0000000..effe6aa --- /dev/null +++ b/back-end/src/Endpoints/SiteLookupEndpoint.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 <https://www.gnu.org/licenses/>. + +using System; +using System.Net; +using System.Text; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Plugins; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Validation; + +using SimpleBookmark.PlatformFeatures.Curl; + +namespace SimpleBookmark.Endpoints +{ + [ConfigurationName("curl")] + internal sealed class SiteLookupEndpoint : ProtectedWebEndpoint + { + const string DefaultCurlExecName = "curl"; + const int MaxTimeoutValue = 30000; + + private readonly SystemCurlApp _curl; + private readonly IAsyncLazy<bool> _isSupported; + + public SiteLookupEndpoint(PluginBase plugin, IConfigScope config) + { + string path = config.GetRequiredProperty("path", p => p.GetString()!); + InitPathAndLog(path, plugin.Log); + + string exePath = config.GetValueOrDefault("exe_path", p => p.GetString(), DefaultCurlExecName); + bool httspOnly = config.GetValueOrDefault("https_only", p => p.GetBoolean(), false); + + //Optional extra arguments + string[] extrArgs = config.GetValueOrDefault( + "extra_args", + p => p.EnumerateArray().Select(s => s.GetString()!).ToArray(), + Array.Empty<string>() + ); + + _curl = new SystemCurlApp(exePath, httspOnly, extrArgs); + + //Immediately check if curl is supported + _isSupported = _curl.TestIsAvailable(plugin.UnloadToken).AsLazy(); + } + + protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) + { + WebMessage webm = new(); + + bool isEnabled = await _isSupported; + + //Allow site to cache if curl is supported on the platform + if (entity.QueryArgs.ContainsKey("support")) + { + webm.Success = isEnabled; + return VirtualOk(entity, webm); + } + + //Assert supported value as curl is required for a normal url lookup + if(webm.Assert(isEnabled, "Curl is not supported on the current platform")) + { + return VirtualClose(entity, webm, HttpStatusCode.NotImplemented); + } + + string? url = entity.QueryArgs.GetValueOrDefault("url"); + + if(webm.Assert(!string.IsNullOrWhiteSpace(url), "No url provided")) + { + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); + } + + if(webm.Assert(UrlFromBase64Url(url!, out Uri? uri), "Invalid url provided")) + { + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); + } + + int? timeoutMs = null; + + //Allow clients to specify a timeout for the request + string? timeoutMsS = entity.QueryArgs.GetValueOrDefault("timeout"); + if (timeoutMsS is not null && int.TryParse(timeoutMsS, out int _timeoutMs)) + { + //Miniumum timeout must be greater than 1 second because curl is timed in seconds + timeoutMs = Math.Clamp(_timeoutMs, 1000, MaxTimeoutValue); + } + + try + { + //Exec curl on the url + CurlResult result = await _curl.ExecLookupAsync(uri!, timeoutMs, entity.EventCancellation); + + if(webm.Assert(result.IsError == false, result.ErrorMessage!)) + { + return VirtualClose(entity, webm, HttpStatusCode.InternalServerError); + } + + webm.Success = true; + webm.Result = result.Result; //Set curl lookup result as the response + + return VirtualOk(entity, webm); + } + catch (TimeoutException) + { + webm.Result = "Request timed out"; + return VirtualClose(entity, webm, HttpStatusCode.InternalServerError); + } + catch (OperationCanceledException) + { + webm.Result = "Request timed out"; + return VirtualClose(entity, webm, HttpStatusCode.InternalServerError); + } + } + + /* + * Reads in a base64url encoded string which is the user's search url and + * attempts to parse it into a uri. If the url is invalid, the function + */ + private static bool UrlFromBase64Url(string base64Url, out Uri? uri) + { + uri = null; + + //Alloc output buffer for decoded data + using UnsafeMemoryHandle<byte> output = MemoryUtil.UnsafeAllocNearestPage(base64Url.Length, true); + + ERRNO decoded = VnEncoding.Base64UrlDecode(base64Url, output.Span, Encoding.UTF8); + if(decoded < 1) + { + return false; + } + + //Recover the url string from its binary representation and try to parse it into a uri + string urlstring = Encoding.UTF8.GetString(output.Span[..(int)decoded]); + return Uri.TryCreate(urlstring, UriKind.Absolute, out uri); + } + } +} diff --git a/back-end/src/PlatformFeatures/Curl/CurlResult.cs b/back-end/src/PlatformFeatures/Curl/CurlResult.cs new file mode 100644 index 0000000..7d70e0e --- /dev/null +++ b/back-end/src/PlatformFeatures/Curl/CurlResult.cs @@ -0,0 +1,19 @@ +// 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/>. + +namespace SimpleBookmark.PlatformFeatures.Curl +{ + internal sealed record class CurlResult(WebsiteLookupResult? Result, bool IsError, string? ErrorMessage); +} diff --git a/back-end/src/PlatformFeatures/Curl/ICurlApp.cs b/back-end/src/PlatformFeatures/Curl/ICurlApp.cs new file mode 100644 index 0000000..ec952e0 --- /dev/null +++ b/back-end/src/PlatformFeatures/Curl/ICurlApp.cs @@ -0,0 +1,34 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace SimpleBookmark.PlatformFeatures.Curl +{ + internal interface ICurlApp + { + /// <summary> + /// Executes a lookup on the given website and returns the title and description + /// </summary> + /// <param name="website">The website url to search against</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The result of the website lookup</returns> + Task<CurlResult> ExecLookupAsync(Uri website, int? timeoutMs, CancellationToken cancellation); + } +} diff --git a/back-end/src/PlatformFeatures/Curl/SystemCurlApp.cs b/back-end/src/PlatformFeatures/Curl/SystemCurlApp.cs new file mode 100644 index 0000000..0949136 --- /dev/null +++ b/back-end/src/PlatformFeatures/Curl/SystemCurlApp.cs @@ -0,0 +1,338 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Diagnostics; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace SimpleBookmark.PlatformFeatures.Curl +{ + sealed class SystemCurlApp(string exePath, bool httpsOnly, string[] additionalArgs) : ISystemApp, ICurlApp + { + const int DefaultTimeoutMs = 5000; + + ///<inheritdoc/> + public async Task<bool> TestIsAvailable(CancellationToken cancellation) + { + try + { + //Test if the curl application is available on the local system, may be at path + using Process? process = Exec(["--version"]); + + if (process is null) + { + return false; + } + + //Wait for the process to exit + await process.WaitForExitAsync(cancellation); + + //If an ok status code, then we know the curl application is available + return process.ExitCode == 0; + } + //App not found + catch (Win32Exception) + { + return false; + } + } + + private Process? Exec(string[] arguments) + { + ProcessStartInfo startInfo = new() + { + FileName = exePath, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + + //Add arguments + arguments.ForEach(startInfo.ArgumentList.Add); + + return Process.Start(startInfo); + } + + private void ValidateUrl(Uri? website) + { + ArgumentNullException.ThrowIfNull(website); + + if (!website.IsAbsoluteUri) + { + throw new ArgumentException("The website url must be an absolute uri", nameof(website)); + } + + if (httpsOnly && website.Scheme != Uri.UriSchemeHttps) + { + throw new ArgumentException("The website url must be an https url only!", nameof(website)); + } + else if (website.Scheme != Uri.UriSchemeHttp && website.Scheme != Uri.UriSchemeHttps) + { + //Http or https only + throw new ArgumentException("The website url must be an http or https url", nameof(website)); + } + } + + ///<inheritdoc/> + public async Task<CurlResult> ExecLookupAsync(Uri website, int? timeoutMs, CancellationToken cancellation) + { + //Validate the url + ValidateUrl(website); + + string timeoutArg = timeoutMs.HasValue ? $"{timeoutMs.Value / 1000}" : $"{DefaultTimeoutMs / 1000}"; + + string[] args = [ + "--max-time", timeoutArg, //Set the max time for the request + "-S", //Silent mode is required + "-H", "Accept: text/html,application/html", //Html is required + ..additionalArgs, //Additional global arguments + website.AbsoluteUri + ]; + + //Execute the curl command + + using Process? process = Exec(args); + + if (process is null) + { + return new CurlResult(null, true, "Curl is not enabled on this platform, lookup failed"); + } + + //Parse the html data + Task<string?> documentHeadTask = HtmlTokenReader.ReadHeadTokenAsync(process.StandardOutput, cancellation); + + //Respect the user's timeout command and termimate the process if it exceeds the timeout + if (timeoutMs.HasValue) + { + await documentHeadTask.WaitAsync(TimeSpan.FromMilliseconds(timeoutMs.Value)); + + await Task.WhenAll( + DiscardStreamAsync(process.StandardOutput, cancellation), + DiscardStreamAsync(process.StandardError, cancellation) + ).WaitAsync(TimeSpan.FromMilliseconds(timeoutMs.Value)); + } + else + { + await documentHeadTask; + + await Task.WhenAll( + DiscardStreamAsync(process.StandardOutput, cancellation), + DiscardStreamAsync(process.StandardError, cancellation) + ); + } + + await process.WaitForExitAsync(cancellation); + + if (process.ExitCode != 0) + { + return new CurlResult(null, true, "Curl exited with a non-zero status code"); + } + + string? documentHead = await documentHeadTask; + + if (documentHead is null) + { + return new CurlResult(null, true, "Failed to parse html data"); + } + + //Get the lookup result from the document head segmetn + WebsiteLookupResult result = HtmlTokenReader.ParseHtmlData(documentHead); + + return new CurlResult(result, false, null); + } + + /// <summary> + /// Safely discards the entire stream of data from the reader without + /// allocating a large string buffer + /// </summary> + /// <param name="reader">The reader to discard</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task that represents the discard opeartion</returns> + private static async Task DiscardStreamAsync(TextReader reader, CancellationToken cancellation) + { + using ArrayPoolBuffer<char> discarBuffer = new(8192); + + while (await reader.ReadBlockAsync(discarBuffer.AsMemory(), cancellation) > 0) + { } + } + + private static class HtmlTokenReader + { + /// <summary> + /// Gets the document title from the head of the html document + /// </summary> + /// <param name="head">The head string containing the title to parse</param> + /// <returns>The title string if found</returns> + public static string? GetDocTitleFromHead(string head) + { + ReadOnlySpan<char> headChars = head.AsSpan(); + + ReadOnlySpan<char> title = headChars.SliceAfterParam("<title>"); + title = title.SliceBeforeParam("</title>"); + + return title.ToString(); + } + + /// <summary> + /// Attempts to get the document summary from the head of the html document + /// in the meta description tag + /// </summary> + /// <param name="head">The head string to parse</param> + /// <returns>The document description if found</returns> + public static string? GetDocumentSummary(string head) + { + ReadOnlySpan<char> headChars = head.AsSpan(); + + ReadOnlySpan<char> desc = headChars.SliceAfterParam("<meta name=\"description\" content=\""); + desc = desc.SliceBeforeParam("\"/>"); + desc = desc.SliceBeforeParam("\">"); + + return desc.ToString(); + } + + /// <summary> + /// Attempts to get the document keywords from the head of the html document + /// by parsing the meta keywords tag + /// </summary> + /// <param name="head">The document head</param> + /// <returns>An array of document keywords found from the head section</returns> + public static string[]? GetDocumentKeywords(string head) + { + ReadOnlySpan<char> headChars = head.AsSpan(); + + ReadOnlySpan<char> kwStart = headChars.SliceAfterParam("<meta name=\"keywords\" content=\""); + ReadOnlySpan<char> kwSpan = kwStart.SliceBeforeParam("\">"); + + List<string> keywords = []; + + //Split the keywords at comma, and remove any empty entries/whitespace + kwSpan.Split(',', keywords, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + return keywords.ToArray(); + } + + public static WebsiteLookupResult ParseHtmlData(string documentHead) + { + //Parse head segments for title, description, and keywords + return new WebsiteLookupResult( + title: GetDocTitleFromHead(documentHead), + description: GetDocumentSummary(documentHead), + keywords: GetDocumentKeywords(documentHead) + ); + } + + + + public static async Task<string?> ReadHeadTokenAsync(TextReader reader, CancellationToken cancellation) + { + //String buffer to store parsed head data + StringBuilder stringBuilder = new(1024); + + //Temp copy buffer + using ArrayPoolBuffer<char> buffer = new(4096); + + bool isStart = true, isEnd = false; + + //scan for docuemnt head + do + { + int read = await reader.ReadBlockAsync(buffer.AsMemory(), cancellation); + + if (read == 0) + { + //Read should never return 0, if it does, then there is no head to read + return null; + } + + if (isStart) + { + Memory<char> headSpan = HeadStart(buffer.AsMemory()); + + //No head was found, continue buffering + if (headSpan.IsEmpty) + { + continue; + } + + /* + * Try to find the end of the head, if it is found, then we can break + */ + isEnd = HeadEnd(ref headSpan); + + //Valid head data to buffer + stringBuilder.Append(headSpan); + + isStart = false; + } + else + { + //Head start was already found, just need to buffer until it ends + Memory<char> end = buffer.AsMemory(); + + isEnd = HeadEnd(ref end); + + stringBuilder.Append(end); + + if (isEnd) + { + break; + } + } + + } while (!isEnd); + + return stringBuilder.ToString(); + } + + static Memory<char> HeadStart(Memory<char> start) + { + //find start of head + int headStartIndex = start.Span.IndexOf("<head>"); + + if (headStartIndex == -1) + { + return default; + } + + return start[headStartIndex..]; + } + + static bool HeadEnd(ref Memory<char> end) + { + //find end of head + int headEndIndex = end.Span.IndexOf("</head>"); + + if (headEndIndex == -1) + { + return false; + } + + end = end[..headEndIndex]; + return true; + } + } + } +} diff --git a/back-end/src/PlatformFeatures/Curl/WebsiteLookupResult.cs b/back-end/src/PlatformFeatures/Curl/WebsiteLookupResult.cs new file mode 100644 index 0000000..e9d9bc0 --- /dev/null +++ b/back-end/src/PlatformFeatures/Curl/WebsiteLookupResult.cs @@ -0,0 +1,23 @@ +// 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/>. + +namespace SimpleBookmark.PlatformFeatures.Curl +{ +#pragma warning disable IDE1006 // Naming Styles (JSON serialization) + + internal sealed record class WebsiteLookupResult(string? title, string? description, string[]? keywords); + +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/back-end/src/PlatformFeatures/ISystemApp.cs b/back-end/src/PlatformFeatures/ISystemApp.cs new file mode 100644 index 0000000..11d15f1 --- /dev/null +++ b/back-end/src/PlatformFeatures/ISystemApp.cs @@ -0,0 +1,31 @@ +// 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.Threading.Tasks; +using System.Threading; + +namespace SimpleBookmark.PlatformFeatures +{ + internal interface ISystemApp + { + /// <summary> + /// Gets a value indicating if the curl application is available + /// on the local system. + /// </summary> + /// <returns>True if the curl exe is available on the local system, false otherwise</returns> + Task<bool> TestIsAvailable(CancellationToken cancellation); + } +} diff --git a/back-end/src/SimpleBookmark.csproj b/back-end/src/SimpleBookmark.csproj index 609144b..1eeaaba 100644 --- a/back-end/src/SimpleBookmark.csproj +++ b/back-end/src/SimpleBookmark.csproj @@ -34,11 +34,11 @@ <ItemGroup> <PackageReference Include="MemoryPack" Version="1.10.0" /> - <PackageReference Include="VNLib.Plugins.Extensions.Data" Version="0.1.0-ci0050" /> - <PackageReference Include="VNLib.Plugins.Extensions.Loading" Version="0.1.0-ci0050" /> - <PackageReference Include="VNLib.Plugins.Extensions.Loading.Sql" Version="0.1.0-ci0050" /> - <PackageReference Include="VNLib.Plugins.Extensions.Validation" Version="0.1.0-ci0050" /> - <PackageReference Include="VNLib.Plugins.Extensions.VNCache" Version="0.1.0-ci0052" /> + <PackageReference Include="VNLib.Plugins.Extensions.Data" Version="0.1.0-ci0052" /> + <PackageReference Include="VNLib.Plugins.Extensions.Loading" Version="0.1.0-ci0052" /> + <PackageReference Include="VNLib.Plugins.Extensions.Loading.Sql" Version="0.1.0-ci0052" /> + <PackageReference Include="VNLib.Plugins.Extensions.Validation" Version="0.1.0-ci0052" /> + <PackageReference Include="VNLib.Plugins.Extensions.VNCache" Version="0.1.0-ci0053" /> </ItemGroup> <ItemGroup> diff --git a/back-end/src/SimpleBookmark.json b/back-end/src/SimpleBookmark.json index 27ebff8..116587d 100644 --- a/back-end/src/SimpleBookmark.json +++ b/back-end/src/SimpleBookmark.json @@ -14,6 +14,19 @@ } }, + //System website lookup endpoint (aka curl) + "curl": { + "path": "/lookup", + "exe_path": "curl", //Path to the curl executable + "extra_args": [ + "--globoff", //Disables unsafe url globbing + "--no-keepalive", //Disables keepalive, uneeded for a single lookup request + "--max-filesize", "100K", //Max file size 100K + "--max-redirs", "5", //Max redirects 5 + "--location", //Follow redirects + ] + }, + "registration": { "path": "/register", //Path for the registration endpoint "token_lifetime_mins": 360, //Token lifetime in minutes diff --git a/back-end/src/SimpleBookmarkEntry.cs b/back-end/src/SimpleBookmarkEntry.cs index a1c9590..13b94a5 100644 --- a/back-end/src/SimpleBookmarkEntry.cs +++ b/back-end/src/SimpleBookmarkEntry.cs @@ -50,9 +50,10 @@ namespace SimpleBookmark //route the bm endpoint this.Route<BookmarkEndpoint>(); this.Route<BmAccountEndpoint>(); + this.Route<SiteLookupEndpoint>(); //Ensure database is created after a delay - this.ObserveWork(() => this.EnsureDbCreatedAsync<SimpleBookmarkContext>(this), 1000); + this.ObserveWork(() => this.EnsureDbCreatedAsync<SimpleBookmarkContext>(this), 1500); Log.Information("Plugin Loaded"); PrintHelloMessage(); diff --git a/ci/config/SimpleBookmark.json b/ci/config/SimpleBookmark.json index 6cb1b93..64be3c1 100644 --- a/ci/config/SimpleBookmark.json +++ b/ci/config/SimpleBookmark.json @@ -14,6 +14,19 @@ } }, + //System website lookup endpoint (aka curl) + "curl": { + "path": "/api/lookup", + "exe_path": "curl", //Path to the curl executable + "extra_args": [ + "--globoff", //Disables unsafe url globbing + "--no-keepalive", //Disables keepalive, uneeded for a single lookup request + "--max-filesize", "100K", //Max file size 100K + "--max-redirs", "5", //Max redirects 5 + "--location", //Follow redirects + ] + }, + "registration": { "path": "/api/register", //Path for the registration endpoint "token_lifetime_mins": 360, //Token lifetime in minutes diff --git a/ci/config/config.json b/ci/config/config.json index e4b33e8..61293b6 100644 --- a/ci/config/config.json +++ b/ci/config/config.json @@ -41,7 +41,7 @@ //Setup the native lib "vnlib.net.compression": { - "lib_path": "lib/vnlib_compress/build/<os-dependent-lib-path>", + "lib_path": "lib/vnlib_compress.dll", "level": 1 }, @@ -95,16 +95,16 @@ //"cors_allowed_authority": [ "localhost:8080" ], //Define a TLS certificate (enables TLS on the interface) - "disabled ssl": { + "ssl": { //Cert may be pem or pfx (include private key in pfx, or include private key in a pem file) - "cert": "/path/to/cert.pfx|pem", + "cert": "ssl/cert.pem", //A pem encoded private key, REQUIRED if using a PEM certificate, may be encrypted with a password - "privkey": "/path/to/private_key.pem", + "privkey": "ssl/key.pem", //An optional password for the ssl private key - "password": "plain-text-password", + //"password": "plain-text-password", //requires that any client connecting to this host present a valid certificate "client_cert_required": false @@ -129,29 +129,29 @@ "assets": "plugins/assets/" }, - "disabled sys_log": { - //"path": "path/to/syslog/file", + "sys_log": { + "path": "data/logs/syslog.txt", //"template": "serilog template for writing to file", - //"flush_sec": 5, - //"retained_files": 31, - //"file_size_limit": 10485760, - //"interval": "infinite" + "flush_sec": 5, + "retained_files": 10, + "file_size_limit": 10485760, + "interval": "infinite" }, "disabled app_log": { - //"path": "path/to/applog/file", + "path": "data/logs/applog.txt", //"template": "serilog template for writing to file", - //"flush_sec": 5, - //"retained_files": 31, - //"file_size_limit": 10485760, - //"interval": "infinite" + "flush_sec": 5, + "retained_files": 10, + "file_size_limit": 10485760, + "interval": "infinite" }, //Sql for the users database "sql": { "debug": false, "provider": "VNLib.Plugins.Extensions.Sql.SQLite.dll", - "source": "simple-bookmark.db" //For sqlite only + "source": "data/simple-bookmark.db" //For sqlite only }, //caching should be setup globally after VNCache #78a47dd diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index 6804a6e..f5ac798 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -32,7 +32,7 @@ COPY app/ /app #pull compiled libs from build container COPY --from=native-cont /build/out /app/lib -RUN apk update && apk add --no-cache gettext icu-libs dumb-init +RUN apk update && apk add --no-cache gettext icu-libs dumb-init curl #workdir WORKDIR /app diff --git a/ci/container/Taskfile.yaml b/ci/container/Taskfile.yaml index 97548dc..557e48d 100644 --- a/ci/container/Taskfile.yaml +++ b/ci/container/Taskfile.yaml @@ -41,13 +41,12 @@ tasks: cmds: # clean up the run.sh script to remove windows line endings in my wsl default instance - cmd: wsl dos2unix ./run.sh - platform: [ win-x64 ] + platforms: [ windows/amd64 ] #init build image - task: setup-container-image #remove the default config file as it's not needed in the container - - powershell -Command "rm -Force build/app/config.json" - powershell -Command "rm -Force -Recurse build/app/config/" - task: prune-sql-runtimes @@ -84,7 +83,7 @@ tasks: #make build directory - powershell -Command "mkdir build, build/app, build/app/config-templates/, build/app/static/ -Force" #copy the existing linux-x64 build to the build folder, this will be the container base - - powershell -Command "cp -Recurse -Force ../build/linux-x64/* build/app/" + - powershell -Command "cp -Recurse -Force ../build/linux-x86_64/* build/app/" #copy local scripts and config data into the build folder - powershell -Command "cp -Force run.sh, Taskfile.yaml build/app/" - powershell -Command "cp -Force Dockerfile, docker-compose.yaml build/" diff --git a/ci/container/config-templates/SimpleBookmark-template.json b/ci/container/config-templates/SimpleBookmark-template.json index a64a10a..c2bf780 100644 --- a/ci/container/config-templates/SimpleBookmark-template.json +++ b/ci/container/config-templates/SimpleBookmark-template.json @@ -14,6 +14,21 @@ } }, + //System website lookup endpoint (aka curl) + "curl": { + "path": "/api/lookup", + "exe_path": "curl", //Path to the curl executable + "extra_args": [ + "--globoff", //Disables unsafe url globbing + "--no-keepalive", //Disables keepalive, uneeded for a single lookup request + "--max-filesize", + "100K", //Max file size 100K + "--max-redirs", + "5", //Max redirects 5 + "--location", //Follow redirects + ] + }, + "registration": { "path": "/api/register", //Path for the registration endpoint "token_lifetime_mins": ${REG_TOKEN_DURATION_MIN}, //Token lifetime in minutes diff --git a/ci/container/docker-compose.yaml b/ci/container/docker-compose.yaml index eb28055..03eb815 100644 --- a/ci/container/docker-compose.yaml +++ b/ci/container/docker-compose.yaml @@ -27,6 +27,7 @@ services: CACHE_ASM_PATH: "VNLib.Data.Caching.Providers.VNCache.dll" MEMCACHE_ONLY: "true" REDIS_CONNECTION_STRING: "" + #at least one node required if MEMCACHE_ONLY is false VNCACHE_INITIAL_NODES: "[]" #ACCOUNTS MAX_LOGIN_ATTEMPS: "10" @@ -35,6 +36,7 @@ services: PASSWORD_PEPPER: "" DATABASE_PASSWORD: "" REDIS_PASSWORD: "" + #if MEMCACHE_ONLY is false, then the following keys are required to connect to a VNCACHE cluster VNCACHE_CLIENT_PRIVATE_KEY: "" VNCACHE_CACHE_PUBLIC_KEY: "" @@ -42,5 +44,5 @@ services: HTTP_DOWNSTREAM_SERVERS: '[]' #SSL_JSON: '{"cert": "ssl/cert.pem", "privkey":"ssl/priv.pem"}' - SERVER_ARGS: "--input-off" + SERVER_ARGS: "" diff --git a/ci/container/run.sh b/ci/container/run.sh index 2c2636c..c780929 100644 --- a/ci/container/run.sh +++ b/ci/container/run.sh @@ -12,4 +12,4 @@ done cp usr/assets/* plugins/assets/ -rf #start the server -dotnet webserver/VNLib.WebServer.dll --config config/config.json $SERVER_ARGS
\ No newline at end of file +dotnet webserver/VNLib.WebServer.dll --config config/config.json --input-off $SERVER_ARGS
\ No newline at end of file diff --git a/ci/plugins.taskfile.yaml b/ci/plugins.taskfile.yaml index f39121d..cab3d53 100644 --- a/ci/plugins.taskfile.yaml +++ b/ci/plugins.taskfile.yaml @@ -15,18 +15,18 @@ tasks: all: deps: - - install-accounts - - install-router - - install-sessions - - install-vncache - - install-vncache-sessions - - install-users - - install-sqlite - - install-argon2-lib - - install-compression - - install-compressor-lib - + - install-rpmalloc + - install-compressor-lib + - install-argon2-lib + - install-compression + - install-sqlite cmds: + - task: install-accounts + - task: install-router + - task: install-sessions + - task: install-vncache + - task: install-vncache-sessions + - task: install-users - echo "Installing and configuring plugins and UI" - task: build-bookmarks @@ -156,3 +156,20 @@ tasks: cmd: powershell -Command "rm ./lib/argon2/{{.ITEM}} -Recurse" ignore_error: true + install-rpmalloc: + cmds: + #install the rpmalloc source code package for Linux and Mac + - task: install:install + vars: + PROJECT_NAME: 'vnlib_rpmalloc' + MODULE_NAME: "VNLib.Core" + FILE_NAME: "src.tgz" + DIR: './lib/vnlib_rpmalloc' + + #install the rpmalloc binary for Windows + - task: install:install + vars: + PROJECT_NAME: 'vnlib_rpmalloc' + MODULE_NAME: "VNLib.Core" + FILE_NAME: "win-x64-release-vnlib_rpmalloc.tgz" + DIR: './lib/vnlib_rpmalloc' diff --git a/ci/release.taskfile.yaml b/ci/release.taskfile.yaml new file mode 100644 index 0000000..f6fdf62 --- /dev/null +++ b/ci/release.taskfile.yaml @@ -0,0 +1,111 @@ +# https://taskfile.dev + +#Inlcuded taskfile for object cache server that is used to produce +#ci builds for standalone caching servers + +version: "3" + +vars: + SSL_DIR: "ssl" + DATA_DIR: "data" + DEFAULT_EC_CURVE: "secp384r1" + +tasks: + default: + desc: "Runs the Simple-Bookmark server" + cmds: + - task: run + + run: + desc: "Runs the Simple-Bookmark server" + silent: true + env: + #libraries intentionally do not have extensions, for cross-platform compatibility, the server will load them regardless + VNLIB_SHARED_HEAP_FILE_PATH: lib/vnlib_rpmalloc.dll + VNLIB_ARGON2_DLL_PATH: lib/argon2.dll + cmds: + - cmd: dotnet webserver/VNLib.WebServer.dll --config config/config.json {{.CLI_ARGS}} + + setup-apt: + desc: "Performs initial setup on Debian apt amd64 based machines" + silent: true + cmds: + - apt update + - apt install -y dotnet-runtime-8.0 gcc cmake curl + - task: setup + - echo "Setup complete" + + setup-dnf: + desc: "Performs initial setup on Fedora/Redhat amd (dnf) based machines" + silent: true + cmds: + - dnf update + - dnf install -y dotnet-runtime-8.0 gcc cmake curl + - task: setup + - echo "Setup complete" + + setup-apk: + desc: "Performs initial setup using the APK package manager for amd64 based machines" + silent: true + cmds: + - apk update + - apk add --no-cache dotnet8-runtime build-base cmake curl + - task: setup + - echo "Setup complete" + + setup: + desc: "Performs platform agnostic setup tasks without installing tools (no sudo needed)" + cmds: + #build rpmalloc lib + - task: build-rpmalloc + - task: build-argon2 + - task: build-compress + + #setup ssl dir + - cmd: mkdir ssl/ + platforms: [ linux, darwin ] + ignore_error: true + - cmd: powershell -Command "mkdir ssl/" + platforms: [ windows/amd64 ] + ignore_error: true + + create-cert: + desc: "Genereates a new self-signed TLS certificate" + cmds: + - openssl req -new -x509 -days 365 -keyout {{.SSL_DIR}}/key.pem -out {{.SSL_DIR}}/cert.pem -newkey ec -pkeyopt ec_paramgen_curve:{{.DEFAULT_EC_CURVE}} --nodes + + build-rpmalloc: + internal: true + dir: 'lib/' + cmds: + #build rpmalloc library for linux/mac + - cmd: cd vnlib_rpmalloc/ && task && cp build/libvn_rpmalloc{{if eq OS "darwin"}}.dylib{{else}}.so{{end}} ../vnlib_rpmalloc.dll + platforms: [ linux, darwin ] + + #for windows just copy the existing dll + - cmd: powershell -Command "cp vnlib_rpmalloc/vnlib_rpmalloc.dll vnlib_rpmalloc.dll" + platforms: [ windows/amd64 ] + + build-argon2: + internal: true + dir: 'lib/' + cmds: + #build argon2 library for linux/mac + - cmd: cd argon2/ && task && cp build/libargon2{{if eq OS "darwin"}}.dylib{{else}}.so{{end}} ../argon2.dll + platforms: [ linux, darwin ] + + #for windows just copy the existing dll + - cmd: powershell -Command "cp argon2/argon2.dll argon2.dll" + platforms: [ windows/amd64 ] + + build-compress: + internal: true + dir: 'lib/' + cmds: + - cd vnlib_compress/ && task + #build the native compressor library for linux/mac + - cmd: cd vnlib_compress/ && cp build/libvn_compress{{if eq OS "darwin"}}.dylib{{else}}.so{{end}} ../vnlib_compress.dll + platforms: [ linux, darwin ] + + - cmd: powershell -Command "cp vnlib_compress/build/Release/vnlib_compress.dll vnlib_compress.dll" + platforms: [ windows/amd64 ]
\ No newline at end of file diff --git a/ci/setup.sh b/ci/setup.sh deleted file mode 100644 index 0cc153b..0000000 --- a/ci/setup.sh +++ /dev/null @@ -1,50 +0,0 @@ -#! /bin/bash - -echo "Testing for go-task" -#test for platform tools -if ! command -v task &> /dev/null -then - echo "You must install go-task: from https://taskfile.dev/installation/" - exit 1 -fi - -echo "Testing for cmake" -#test for cmake -if ! command -v cmake &> /dev/null -then - echo "You must have cmake installed globally" - exit 1 -fi - -echo "Testing for GNUMake" -#test for make -if ! command -v make &> /dev/null -then - echo "You must have GNUMake installed globally" - exit 1 -fi - -echo "Testing for git" -#test for git -if ! command -v git &> /dev/null -then - echo "You must have git installed globally" - exit 1 -fi - -#build the argon2 native library -pushd argon2 > /dev/null -echo "Building Argon2 native library" -make -argon2_path=$(find "$(pwd)" -iname "libargon2.so.*") - -echo "Add the following environment variable" -echo VNLIB_ARGON2_DLL_PATH=$argon2_path -popd > /dev/null - -#build the vnlib_compress native library -pushd vnlib_compress > /dev/null -echo "Building vnlib_compress native library" -task -echo "Finished building vnlib_compress" -popd > /dev/null
\ No newline at end of file diff --git a/ci/taskfile.yaml b/ci/taskfile.yaml index a27b1ac..43e11a8 100644 --- a/ci/taskfile.yaml +++ b/ci/taskfile.yaml @@ -7,6 +7,7 @@ version: "3" vars: BUILDS_URL: https://www.vaughnnugent.com/public/resources/software/builds + SQLITE_OUT_DIR: "plugins/assets/VNLib.Plugins.Extensions.Loading.Sql.SQLite" includes: install: @@ -30,22 +31,33 @@ tasks: - cmd: powershell -Command "rm -Recurse -Force ./dist" ignore_error: true - #copy setup script for linux - cmd: powershell -Command "mkdir lib -Force" ignore_error: true - - cmd: wsl dos2unix ./setup.sh #convert the setup script to unix line endings for linux - platform: [ win-x64 ] - - powershell -Command "cp setup.sh lib/ -Force" - - task: install-plugins + - task: plugins:all - task: install-webserver + - task: prune-runtimes #run container build last - task: container:build install-webserver: cmds: - - for: [ win-x64, linux-x64, osx-x64 ] + - cmd : powershell -Command "mkdir webserver -Force" + ignore_error: true + + #clone the webserver (it's cross platform when using dotnet command) + - task: install:install + vars: + PROJECT_NAME: 'VNLib.Webserver' + MODULE_NAME: "VNLib.Webserver" + FILE_NAME: "linux-x64-release.tgz" + DIR: 'webserver/' + + #remove the executable since its not needed + - cmd: cd webserver/ && powershell -Command "rm VNlib.WebServer" + + - for: [ windows-x86_64, linux-x86_64, osx-x86_64, windows-arm, linux-arm, osx-arm ] task: create-env vars: TARGET_OS: '{{.ITEM}}' @@ -55,7 +67,7 @@ tasks: #make bin dir - cmd: powershell -Command "mkdir bin -Force" ignore_error: true - - for: [ win-x64, linux-x64, osx-x64 ] + - for: [ windows-x86_64, linux-x86_64, osx-x86_64, windows-arm, linux-arm, osx-arm ] task: pack vars: TARGET_OS: '{{.ITEM}}' @@ -66,11 +78,6 @@ tasks: ignore_error: true - task: container:postbuild_success - - install-plugins: - cmds: - #add plugins - - task: plugins:all build-container: cmds: @@ -84,39 +91,57 @@ tasks: - cmd: powershell -Command "mkdir {{.BUILD_DIR}} -Force" ignore_error: true - #copy build files - - for: [ plugins, dist, lib, config ] + #copy build files for target os + - for: [ plugins, dist, lib, config, webserver ] cmd: powershell -Command "cp -Recurse -Force {{.ITEM}} {{.BUILD_DIR}}" - - task: get-webserver - vars: - TARGET_OS: '{{.TARGET_OS}}' - BUILD_DIR: '{{.BUILD_DIR}}' - - get-webserver: - internal: true - cmds: - - task: install:install - vars: - PROJECT_NAME: 'VNLib.Webserver' - MODULE_NAME: "VNLib.Webserver" - FILE_NAME: "{{.TARGET_OS}}-release.tgz" - DIR: '{{.BUILD_DIR}}/webserver' - - - cmd: powershell -Command "cp -Force ./config/config.json {{.BUILD_DIR}}/config.json" + #copy release taskfile and rename it + - cmd: powershell -Command "cp -Force release.taskfile.yaml {{.BUILD_DIR}}/Taskfile.yaml" pack: internal: true cmds: - cmd: powershell -Command "mkdir build/{{.TARGET_OS}}/ -Force" ignore_error: true - - cd build/{{.TARGET_OS}} && tar -czf ../../bin/{{.TARGET_OS}}-release.tgz . + - cd build/{{.TARGET_OS}} && tar -czf ../../bin/{{.TARGET_OS}}-release.tgz . + + prune-runtimes: + cmds: + #prune sqlite runtime native libraries that Im not targeting + #windows + - for: ['browser-wasm', 'linux-arm', 'linux-arm64', 'linux-armel', 'linux-mips64', 'linux-musl-arm', 'linux-musl-arm64', 'linux-musl-x64', 'linux-ppc64le', 'linux-s390x', 'linux-x64', 'linux-x86', 'maccatalyst-arm64', 'maccatalyst-x64', 'osx-arm64', 'osx-x64', 'win-arm', 'win-arm64' ] + cmd: cd build/windows-x86_64/{{.SQLITE_OUT_DIR}}/runtimes && powershell -Command "rm {{.ITEM}} -Recurse -Force" + ignore_error: true + + #windows arm + - for: ['browser-wasm', 'linux-arm', 'linux-arm64', 'linux-armel', 'linux-mips64', 'linux-musl-arm', 'linux-musl-arm64', 'linux-musl-x64', 'linux-ppc64le', 'linux-s390x', 'linux-x64', 'linux-x86', 'maccatalyst-arm64', 'maccatalyst-x64', 'osx-arm64', 'osx-x64', 'win-x86', 'win-x64' ] + cmd: cd build/windows-arm/{{.SQLITE_OUT_DIR}}/runtimes && powershell -Command "rm {{.ITEM}} -Recurse -Force" + ignore_error: true + + #linux x64 + - for: ['browser-wasm', 'linux-arm', 'linux-arm64', 'linux-armel', 'linux-musl-arm', 'linux-musl-arm64', 'maccatalyst-arm64', 'maccatalyst-x64', 'osx-arm64', 'osx-x64', 'win-arm', 'win-arm64', 'win-x86', 'win-x64' ] + cmd: cd build/linux-x86_64/{{.SQLITE_OUT_DIR}}/runtimes && powershell -Command "rm {{.ITEM}} -Recurse -Force" + ignore_error: true + #linux arm + - for: ['browser-wasm', 'linux-mips64', 'linux-musl-x64', 'linux-ppc64le', 'linux-s390x', 'linux-x64', 'linux-x86', 'maccatalyst-arm64', 'maccatalyst-x64', 'osx-arm64', 'osx-x64', 'win-arm', 'win-arm64', 'win-x86', 'win-x64' ] + cmd: cd build/linux-arm/{{.SQLITE_OUT_DIR}}/runtimes && powershell -Command "rm {{.ITEM}} -Recurse -Force" + ignore_error: true + + #osx x64 + - for: ['browser-wasm', 'linux-arm', 'linux-arm64', 'linux-armel', 'linux-mips64', 'linux-musl-arm', 'linux-musl-arm64', 'linux-musl-x64', 'linux-ppc64le', 'linux-s390x', 'linux-x64', 'linux-x86', 'maccatalyst-arm64', 'win-arm', 'win-arm64', 'win-x86', 'win-x64' ] + cmd: cd build/osx-x86_64/{{.SQLITE_OUT_DIR}}/runtimes && powershell -Command "rm {{.ITEM}} -Recurse -Force" + ignore_error: true + + #osx arm + - for: ['browser-wasm', 'linux-arm', 'linux-arm64', 'linux-armel', 'linux-mips64', 'linux-musl-arm', 'linux-musl-arm64', 'linux-musl-x64', 'linux-ppc64le', 'linux-s390x', 'linux-x64', 'linux-x86', 'maccatalyst-x64', 'osx-x64', 'win-arm', 'win-arm64', 'win-x86', 'win-x64' ] + cmd: cd build/osx-arm/{{.SQLITE_OUT_DIR}}/runtimes && powershell -Command "rm {{.ITEM}} -Recurse -Force" + ignore_error: true clean: ignore_error: true cmds: - - for: [ ./build, ./bin, ./dist, ./plugins, ./lib ] + - for: [ build/, bin/, dist/, plugins/, lib/, webserver/ ] cmd: powershell -Command "rm -Recurse -Force '{{.ITEM}}'" - task: container:clean
\ No newline at end of file diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 9231ae5..856b959 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -10,7 +10,7 @@ "license": "agpl3", "dependencies": { "@headlessui/vue": "^1.7.17", - "@vnuge/vnlib.browser": "https://www.vaughnnugent.com/public/resources/software/builds/Plugins.Essentials/eb9752ab262522271ccaf1ff127658b7202289a4/@vnuge-vnlib.browser/release.tgz", + "@vnuge/vnlib.browser": "https://www.vaughnnugent.com/public/resources/software/builds/Plugins.Essentials/f2ac807486a00db4ba8486133d567e392f0fe98a/@vnuge-vnlib.browser/release.tgz", "@vuelidate/core": "^2.0.2", "@vuelidate/validators": "^2.0.2", "@vueuse/core": "^10.3.x", @@ -62,9 +62,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -588,14 +588,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz", - "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -611,9 +611,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -625,9 +625,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", - "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -686,9 +686,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", - "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz", + "integrity": "sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==", "cpu": [ "arm" ], @@ -699,9 +699,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", - "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.1.tgz", + "integrity": "sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==", "cpu": [ "arm64" ], @@ -712,9 +712,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", - "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.1.tgz", + "integrity": "sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==", "cpu": [ "arm64" ], @@ -725,9 +725,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", - "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.1.tgz", + "integrity": "sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==", "cpu": [ "x64" ], @@ -738,9 +738,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", - "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.1.tgz", + "integrity": "sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==", "cpu": [ "arm" ], @@ -751,9 +751,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", - "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.1.tgz", + "integrity": "sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==", "cpu": [ "arm64" ], @@ -764,9 +764,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", - "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.1.tgz", + "integrity": "sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==", "cpu": [ "arm64" ], @@ -777,9 +777,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", - "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.1.tgz", + "integrity": "sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==", "cpu": [ "riscv64" ], @@ -790,9 +790,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", - "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.1.tgz", + "integrity": "sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==", "cpu": [ "x64" ], @@ -803,9 +803,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", - "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.1.tgz", + "integrity": "sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==", "cpu": [ "x64" ], @@ -816,9 +816,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", - "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.1.tgz", + "integrity": "sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==", "cpu": [ "arm64" ], @@ -829,9 +829,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", - "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.1.tgz", + "integrity": "sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==", "cpu": [ "ia32" ], @@ -842,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", - "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.1.tgz", + "integrity": "sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==", "cpu": [ "x64" ], @@ -855,20 +855,20 @@ ] }, "node_modules/@tanstack/virtual-core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.1.2.tgz", - "integrity": "sha512-DATZJs8iejkIUqXZe6ruDAnjFo78BKnIIgqQZrc7CmEFqfLEN/TPD91n4hRfo6hpRB6xC00bwKxv7vdjFNEmOg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.1.3.tgz", + "integrity": "sha512-Y5B4EYyv1j9V8LzeAoOVeTg0LI7Fo5InYKgAjkY1Pu9GjtUwX/EKxNcU7ng3sKr99WEf+bPTcktAeybyMOYo+g==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/vue-virtual": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.1.2.tgz", - "integrity": "sha512-RmUnhsFtRw9p4Ti/+rG2Hr3y4yFhs8Xdsn7x9tkPoKINbVya/5RSCoNUCCAg2iXNjOI5a55iBNzNV0SVwxMwKA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.1.3.tgz", + "integrity": "sha512-OoRCSgp8Bc85Te3pg4OHFUukbWZeB25/O5rNd7MgMtrYIfJjNOaicZeJcvwqK6lDVTMpzohWUMVK/loqR1H8ig==", "dependencies": { - "@tanstack/virtual-core": "3.1.2" + "@tanstack/virtual-core": "3.1.3" }, "funding": { "type": "github", @@ -930,8 +930,8 @@ }, "node_modules/@vnuge/vnlib.browser": { "version": "0.1.13", - "resolved": "https://www.vaughnnugent.com/public/resources/software/builds/Plugins.Essentials/eb9752ab262522271ccaf1ff127658b7202289a4/@vnuge-vnlib.browser/release.tgz", - "integrity": "sha512-yqJXL0H8g27KoCijlPXSG75ZxfWab4cFVdT2t2b+iodpHeytZHemlNteTubzMurA8WRg95WW3Z3mf5R184UnZA==", + "resolved": "https://www.vaughnnugent.com/public/resources/software/builds/Plugins.Essentials/f2ac807486a00db4ba8486133d567e392f0fe98a/@vnuge-vnlib.browser/release.tgz", + "integrity": "sha512-j3BwCdXWJ46Q7GohS+rZg7M5k1/AS+uuycP7wY8RWI2YBKS80uTE6jbWZ0OuCybclBrCufvW7SlTTpfsbf33mw==", "license": "MIT", "peerDependencies": { "@vueuse/core": "^10.x", @@ -940,8 +940,7 @@ "jose": "^5.x", "lodash-es": "^4.x", "universal-cookie": "^7.0.x", - "vue": "^3.x", - "vue-router": "^4.x" + "vue": "^3.x" } }, "node_modules/@volar/language-core": { @@ -973,49 +972,49 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz", - "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz", + "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==", "dependencies": { "@babel/parser": "^7.23.9", - "@vue/shared": "3.4.19", + "@vue/shared": "3.4.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz", - "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz", + "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==", "dependencies": { - "@vue/compiler-core": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-core": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz", - "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz", + "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==", "dependencies": { "@babel/parser": "^7.23.9", - "@vue/compiler-core": "3.4.19", - "@vue/compiler-dom": "3.4.19", - "@vue/compiler-ssr": "3.4.19", - "@vue/shared": "3.4.19", + "@vue/compiler-core": "3.4.21", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21", "estree-walker": "^2.0.2", - "magic-string": "^0.30.6", - "postcss": "^8.4.33", + "magic-string": "^0.30.7", + "postcss": "^8.4.35", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz", - "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz", + "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==", "dependencies": { - "@vue/compiler-dom": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-dom": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/devtools-api": { @@ -1073,48 +1072,48 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz", - "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", "dependencies": { - "@vue/shared": "3.4.19" + "@vue/shared": "3.4.21" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz", - "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", "dependencies": { - "@vue/reactivity": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz", - "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", "dependencies": { - "@vue/runtime-core": "3.4.19", - "@vue/shared": "3.4.19", + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz", - "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", "dependencies": { - "@vue/compiler-ssr": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" }, "peerDependencies": { - "vue": "3.4.19" + "vue": "3.4.21" } }, "node_modules/@vue/shared": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", - "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==" + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", + "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, "node_modules/@vuelidate/core": { "version": "2.0.3", @@ -1201,13 +1200,13 @@ } }, "node_modules/@vueuse/core": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.8.0.tgz", - "integrity": "sha512-G9Ok9fjx10TkNIPn8V1dJmK1NcdJCtYmDRyYiTMUyJ1p0Tywc1zmOoCQ2xhHYyz8ULBU4KjIJQ9n+Lrty74iVw==", + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", + "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.8.0", - "@vueuse/shared": "10.8.0", + "@vueuse/metadata": "10.9.0", + "@vueuse/shared": "10.9.0", "vue-demi": ">=0.14.7" }, "funding": { @@ -1240,17 +1239,17 @@ } }, "node_modules/@vueuse/metadata": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.8.0.tgz", - "integrity": "sha512-Nim/Vle5OgXcXhAvGOgkJQXB1Yb+Kq/fMbLuv3YYDYbiQrwr39ljuD4k9fPeq4yUyokYRo2RaNQmbbIMWB/9+w==", + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", + "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.8.0.tgz", - "integrity": "sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==", + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.9.0.tgz", + "integrity": "sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==", "dependencies": { "vue-demi": ">=0.14.7" }, @@ -1377,9 +1376,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", "dev": true, "funding": [ { @@ -1396,8 +1395,8 @@ } ], "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -1521,9 +1520,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001589", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", - "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", "dev": true, "funding": [ { @@ -1757,9 +1756,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.681", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz", - "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==", + "version": "1.4.700", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.700.tgz", + "integrity": "sha512-40dqKQ3F7C8fbBEmjSeJ+qEHCKzPyrP9SkeIBZ3wSCUH9nhWStrDz030XlDzlhNhlul1Z0fz7TpDFnsIzo4Jtg==", "dev": true }, "node_modules/emoji-regex": { @@ -2254,9 +2253,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -2433,9 +2432,9 @@ } }, "node_modules/jose": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.2.tgz", - "integrity": "sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -2560,9 +2559,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -3224,9 +3223,9 @@ } }, "node_modules/rollup": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", - "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.1.tgz", + "integrity": "sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -3239,19 +3238,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.12.0", - "@rollup/rollup-android-arm64": "4.12.0", - "@rollup/rollup-darwin-arm64": "4.12.0", - "@rollup/rollup-darwin-x64": "4.12.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", - "@rollup/rollup-linux-arm64-gnu": "4.12.0", - "@rollup/rollup-linux-arm64-musl": "4.12.0", - "@rollup/rollup-linux-riscv64-gnu": "4.12.0", - "@rollup/rollup-linux-x64-gnu": "4.12.0", - "@rollup/rollup-linux-x64-musl": "4.12.0", - "@rollup/rollup-win32-arm64-msvc": "4.12.0", - "@rollup/rollup-win32-ia32-msvc": "4.12.0", - "@rollup/rollup-win32-x64-msvc": "4.12.0", + "@rollup/rollup-android-arm-eabi": "4.12.1", + "@rollup/rollup-android-arm64": "4.12.1", + "@rollup/rollup-darwin-arm64": "4.12.1", + "@rollup/rollup-darwin-x64": "4.12.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.1", + "@rollup/rollup-linux-arm64-gnu": "4.12.1", + "@rollup/rollup-linux-arm64-musl": "4.12.1", + "@rollup/rollup-linux-riscv64-gnu": "4.12.1", + "@rollup/rollup-linux-x64-gnu": "4.12.1", + "@rollup/rollup-linux-x64-musl": "4.12.1", + "@rollup/rollup-win32-arm64-msvc": "4.12.1", + "@rollup/rollup-win32-ia32-msvc": "4.12.1", + "@rollup/rollup-win32-x64-msvc": "4.12.1", "fsevents": "~2.3.2" } }, @@ -3656,9 +3655,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -3723,9 +3722,9 @@ "dev": true }, "node_modules/vite": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", - "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", "dev": true, "dependencies": { "esbuild": "^0.19.3", @@ -3778,15 +3777,15 @@ } }, "node_modules/vue": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz", - "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==", + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", "dependencies": { - "@vue/compiler-dom": "3.4.19", - "@vue/compiler-sfc": "3.4.19", - "@vue/runtime-dom": "3.4.19", - "@vue/server-renderer": "3.4.19", - "@vue/shared": "3.4.19" + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" }, "peerDependencies": { "typescript": "*" @@ -3821,21 +3820,6 @@ "eslint": ">=6.0.0" } }, - "node_modules/vue-router": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz", - "integrity": "sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==", - "peer": true, - "dependencies": { - "@vue/devtools-api": "^6.5.1" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, "node_modules/vue-template-compiler": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", @@ -3864,9 +3848,9 @@ } }, "node_modules/vue3-otp-input": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/vue3-otp-input/-/vue3-otp-input-0.4.1.tgz", - "integrity": "sha512-wVl9i3DcWlO0C7fBI9V+RIP3crm/1tY72fuhvb3YM2JfbLoYofB96aPl5AgFhA0Cse5bQEMYtIvOeiqW3rfbAw==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/vue3-otp-input/-/vue3-otp-input-0.4.4.tgz", + "integrity": "sha512-LI1MeBiiEy59cnjqXzlcz4G4cMxZcHF/xOKilb6sfw4uFHfQ22Luu2ls0Bb51zL0pb3gGp7RuIL5eurEJXkoBg==", "engines": { "node": ">=16.0.0", "npm": ">=8.0.0" @@ -3996,9 +3980,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz", - "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", "dev": true, "bin": { "yaml": "bin.mjs" diff --git a/front-end/package.json b/front-end/package.json index 30accd8..0631eae 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -14,11 +14,13 @@ "dev": "vite", "watch": "vite build --watch --mode development --minify false", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "update": "npm update", + "oudated": "npm outdated" }, "dependencies": { "@headlessui/vue": "^1.7.17", - "@vnuge/vnlib.browser": "https://www.vaughnnugent.com/public/resources/software/builds/Plugins.Essentials/eb9752ab262522271ccaf1ff127658b7202289a4/@vnuge-vnlib.browser/release.tgz", + "@vnuge/vnlib.browser": "https://www.vaughnnugent.com/public/resources/software/builds/Plugins.Essentials/f2ac807486a00db4ba8486133d567e392f0fe98a/@vnuge-vnlib.browser/release.tgz", "@vuelidate/core": "^2.0.2", "@vuelidate/validators": "^2.0.2", "@vueuse/core": "^10.3.x", diff --git a/front-end/src/buttons.scss b/front-end/src/buttons.scss index 7088deb..44df2c2 100644 --- a/front-end/src/buttons.scss +++ b/front-end/src/buttons.scss @@ -1,5 +1,13 @@ .btn{ - @apply focus:ring-2 focus:outline-none font-medium rounded text-sm px-4 py-2 text-center text-white; + @apply focus:ring-2 focus:outline-none font-medium rounded text-sm px-2.5 py-2 text-center text-white; + + &.sm{ + @apply text-xs px-2 py-1; + } + + &.lg{ + @apply text-lg px-4 py-3; + } &.round{ @apply rounded-full; diff --git a/front-end/src/components/Bookmarks.vue b/front-end/src/components/Bookmarks.vue index cc3cd6a..274b0b4 100644 --- a/front-end/src/components/Bookmarks.vue +++ b/front-end/src/components/Bookmarks.vue @@ -387,7 +387,6 @@ const upload = (() => { <span class="sr-only">Search</span> </button> </form> - </div> <div class="relative ml-3 md:ml-10"> diff --git a/front-end/src/components/Boomarks/AddOrUpdateForm.vue b/front-end/src/components/Boomarks/AddOrUpdateForm.vue index a4a3f1d..0370e0c 100644 --- a/front-end/src/components/Boomarks/AddOrUpdateForm.vue +++ b/front-end/src/components/Boomarks/AddOrUpdateForm.vue @@ -1,6 +1,8 @@ <script setup lang="ts"> import { computed, toRefs } from 'vue'; -import { join, split } from 'lodash-es'; +import { isEmpty, join, split } from 'lodash-es'; +import { useStore } from '../../store'; +import { useWait } from '@vnuge/vnlib.browser'; const emit = defineEmits(['submit']) const props = defineProps<{ @@ -15,54 +17,110 @@ const tags = computed({ set: (value:string) => v$.value.Tags.$model = split(value, ',') }) +const { websiteLookup:lookup } = useStore() +const { setWaiting, waiting } = useWait() + +const execLookup = async () => { + //url must be valid before searching + if(v$.value.Url.$invalid) return + + setWaiting(true) + + try{ + const { title, description, keywords } = await lookup.execLookup(v$.value.Url.$model); + + //Set the title and description + if(title){ + v$.value.Name.$model = title; + v$.value.Name.$dirty = true; + } + + if(description){ + v$.value.Description.$model = description; + v$.value.Description.$dirty = true; + } + + if(!isEmpty(keywords)){ + v$.value.Tags.$model = keywords; + v$.value.Tags.$dirty = true; + } + } + catch(e){ + //Mostly ignore errors + console.error(e) + } + finally{ + setWaiting(false) + } +} + +const showSearchButton = computed(() => lookup.isSupported && !isEmpty(v$.value.Url.$model)) + </script> <template> - <form class="grid grid-cols-1 gap-4 p-4" @submit.prevent="emit('submit')"> + <form id="bm-add-or-update-form" class="grid grid-cols-1 gap-4 p-4" @submit.prevent="emit('submit')"> <fieldset> - <label for="url" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">URL</label> - <input type="text" id="url" class="input" placeholder="https://www.example.com" - v-model="v$.Url.$model" - :class="{'dirty': v$.Url.$dirty, 'error': v$.Url.$invalid}" - required - > + <label for="url" class="flex justify-between mb-2 text-sm font-medium text-gray-900 dark:text-white"> + URL + </label> + <div class="flex gap-2"> + <input type="text" id="url" class="input" placeholder="https://www.example.com" v-model="v$.Url.$model" + :class="{'dirty': v$.Url.$dirty, 'error': v$.Url.$invalid}" required> + + <div class=""> + <button + type="button" + :disabled="!showSearchButton || waiting" + @click.self.prevent="execLookup" + id="search-btn" + class="btn blue search-btn" + > + <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 20 20"> + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" /> + </svg> + </button> + </div> + </div> </fieldset> <fieldset> <label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label> - <input type="text" id="Name" class="input" placeholder="Hello World" - v-model="v$.Name.$model" - :class="{'dirty': v$.Name.$dirty, 'error': v$.Name.$invalid}" - required - > + <input type="text" id="Name" class="input" placeholder="Hello World" v-model="v$.Name.$model" + :class="{'dirty': v$.Name.$dirty, 'error': v$.Name.$invalid}" required> </fieldset> - <fieldset> + <fieldset> <label for="tags" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Tags</label> - <input type="text" id="tags" class="input" placeholder="tag1,tag2,tag3" - v-model="tags" - :class="{'dirty': v$.Tags.$dirty, 'error': v$.Tags.$invalid}" - > + <input type="text" id="tags" class="input" placeholder="tag1,tag2,tag3" v-model="tags" + :class="{'dirty': v$.Tags.$dirty, 'error': v$.Tags.$invalid}"> </fieldset> <fieldset> - <label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label> + <label for="description" + class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label> <textarea type="text" id="description" rows="5" class="input" placeholder="This is a bookmark" v-model="v$.Description.$model" - :class="{'dirty': v$.Description.$dirty, 'error': v$.Description.$invalid}" - /> + :class="{'dirty': v$.Description.$dirty, 'error': v$.Description.$invalid}" /> </fieldset> - + <div class="flex justify-end"> - <button type="submit" class="btn blue"> - Submit + <button id="save-button" type="submit" form="bm-add-or-update-form" class="btn blue"> + Save </button> </div> </form> </template> -<style scoped lang="scss">input.search { - @apply ps-10 p-2.5 border block w-full text-sm rounded; - @apply bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500; -} +<style scoped lang="scss"> + +#bm-add-or-update-form { + .search-btn{ -button.search { - @apply p-2.5 ms-2 text-sm font-medium text-white bg-blue-700 rounded border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 + @apply my-auto px-3 py-2.5; + + &:disabled{ + @apply bg-gray-600; + } + } } + </style>
\ No newline at end of file diff --git a/front-end/src/components/Settings.vue b/front-end/src/components/Settings.vue index 83d3f79..504f38a 100644 --- a/front-end/src/components/Settings.vue +++ b/front-end/src/components/Settings.vue @@ -19,7 +19,7 @@ const darkMode = useDark(); <h2 class="text-2xl font-bold">Settings</h2> <div class="flex flex-col w-full max-w-3xl gap-10 mt-3"> - <div class=""> + <div class="mb-6"> <h3 class="text-xl font-bold"> General </h3> @@ -41,7 +41,7 @@ const darkMode = useDark(); </div> </div> - <div class=""> + <div class="mb-6"> <h3 class="text-xl font-bold">Boomarks</h3> <div class="relative mt-4"> @@ -51,7 +51,7 @@ const darkMode = useDark(); <PasswordReset /> - <div class=""> + <div class="mb-8"> <h3 class="text-xl font-bold">Multi Factor Auth</h3> <div class="relative mt-4 py-2.5"> @@ -66,7 +66,7 @@ const darkMode = useDark(); <Oauth2Apps /> </div> - <div v-if="store.registation.status?.can_invite" class="mb-10"> + <div v-if="store.registation.status?.can_invite" class="mt-6 mb-10"> <Registation /> </div> diff --git a/front-end/src/components/Settings/Bookmarks.vue b/front-end/src/components/Settings/Bookmarks.vue index a4ab55a..aa4ed31 100644 --- a/front-end/src/components/Settings/Bookmarks.vue +++ b/front-end/src/components/Settings/Bookmarks.vue @@ -1,13 +1,14 @@ <script setup lang="ts"> import { apiCall, useWait } from '@vnuge/vnlib.browser'; import { useStore, type DownloadContentType, TabId } from '../../store'; -import { ref } from 'vue'; +import { computed, ref } from 'vue'; import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' -const { bookmarks } = useStore(); +const { bookmarks, websiteLookup } = useStore(); const downloadAnchor = ref(); const { waiting } = useWait() +const curlSupported = computed(() => websiteLookup.isSupported); const downloadBookmarks = (contentType: DownloadContentType) => { apiCall(async () => { @@ -58,8 +59,10 @@ javascript: (function() { </div> <p class="p-0.5 my-auto text-sm flex flex-row"> <span class=""> - <svg class="w-6 h-5 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> - <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/> + <svg class="w-6 h-5 text-gray-800 dark:text-white" aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + d="M5 12h14M5 12l4-4m-4 4 4 4" /> </svg> </span> <span> @@ -72,47 +75,66 @@ javascript: (function() { <MenuButton :disabled="waiting" class="flex items-center gap-3 btn light"> <div class="hidden lg:inline">Download</div> <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"> - <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4c0 .6.4 1 1 1h14c.6 0 1-.4 1-1v-4c0-.6-.4-1-1-1h-2m-1-5-4 5-4-5m9 8h0"/> + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4c0 .6.4 1 1 1h14c.6 0 1-.4 1-1v-4c0-.6-.4-1-1-1h-2m-1-5-4 5-4-5m9 8h0" /> </svg> </MenuButton> - <transition - enter-active-class="transition duration-100 ease-out" - enter-from-class="transform scale-95 opacity-0" - enter-to-class="transform scale-100 opacity-100" + <transition enter-active-class="transition duration-100 ease-out" + enter-from-class="transform scale-95 opacity-0" enter-to-class="transform scale-100 opacity-100" leave-active-class="transition duration-75 ease-out" - leave-from-class="transform scale-100 opacity-100" - leave-to-class="transform scale-95 opacity-0" - > - <MenuItems class="absolute z-10 bg-white divide-y divide-gray-100 rounded-b shadow right-2 lg:left-0 min-w-32 lg:end-0 dark:bg-gray-700"> - <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton"> + leave-from-class="transform scale-100 opacity-100" leave-to-class="transform scale-95 opacity-0"> + <MenuItems + class="absolute z-10 bg-white divide-y divide-gray-100 rounded-b shadow right-2 lg:left-0 min-w-32 lg:end-0 dark:bg-gray-700"> + <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" + aria-labelledby="dropdownDefaultButton"> <!-- Use the `active` state to conditionally style the active item. --> <MenuItem as="template" v-slot="{ }"> - <li> - <button @click="downloadBookmarks('text/html')" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> - HTML - </button> - </li> + <li> + <button @click="downloadBookmarks('text/html')" + class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> + HTML + </button> + </li> </MenuItem> <MenuItem as="template" v-slot="{ }"> - <li> - <button @click="downloadBookmarks('text/csv')" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> - CSV - </button> - </li> + <li> + <button @click="downloadBookmarks('text/csv')" + class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> + CSV + </button> + </li> </MenuItem> - <MenuItem as="template" v-slot="{ }"> - <li> - <button @click="downloadBookmarks('application/json')" class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> - JSON - </button> - </li> + <MenuItem as="template" v-slot="{ }"> + <li> + <button @click="downloadBookmarks('application/json')" + class="block w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> + JSON + </button> + </li> </MenuItem> </ul> </MenuItems> </transition> </Menu> </div> + <div class="mt-3"> + <h4 class="mb-2 font-bold">Features</h4> + <p class="text-sm text-gray-500 dark:text-gray-400"> + Some features for Simple-Bookmark use tools and applications that are already installed on + your server such as curl. + </p> + <div class="flex flex-row gap-2 mt-4"> + <span class="w-3 h-3 my-auto rounded-full" :class="[curlSupported ? 'bg-green-500' : 'bg-amber-500']"></span> + <span class="my-auto font-bold"> + curl + </span> + <span class="my-auto text-sm text-gray-500 ms-4 dark:text-gray-400"> + Curl is used to fetch website details like title, description and tags. + {{ curlSupported ? '(supported)' : '(not supported)' }} + </span> + </div> + </div> </div> - + <a ref="downloadAnchor" class="hidden"></a> </template>
\ No newline at end of file diff --git a/front-end/src/components/Settings/PkiSettings.vue b/front-end/src/components/Settings/PkiSettings.vue index dfa4cad..885b2cb 100644 --- a/front-end/src/components/Settings/PkiSettings.vue +++ b/front-end/src/components/Settings/PkiSettings.vue @@ -32,6 +32,8 @@ const removeKey = async (key: PkiPublicKey) => { title: 'Key Removed', text: `${key.kid} has been successfully removed` }) + + store.mfaRefreshMethods() }) } @@ -117,7 +119,8 @@ const onAddKey = async () => { text: result }) - hideAddKeyDialog() + hideAddKeyDialog(); + store.mfaRefreshMethods(); }) } diff --git a/front-end/src/components/Settings/Registation.vue b/front-end/src/components/Settings/Registation.vue index a0f208e..d0dfaa7 100644 --- a/front-end/src/components/Settings/Registation.vue +++ b/front-end/src/components/Settings/Registation.vue @@ -58,7 +58,7 @@ const onCancel = () => { <div class=""> <div class="flex flex-row justify-between w-full"> - <h3 class="text-xl font-bold">Registation</h3> + <h3 class="text-xl font-bold">Invite Links</h3> <div class="flex flex-row justify-end"> <button class="btn blue" @click="toggleOpen(true)">Invite User</button> diff --git a/front-end/src/main.ts b/front-end/src/main.ts index c5be406..2f2ca8e 100644 --- a/front-end/src/main.ts +++ b/front-end/src/main.ts @@ -30,6 +30,7 @@ import { mfaSettingsPlugin } from './store/mfaSettingsPlugin' import { socialMfaPlugin } from './store/socialMfaPlugin' import { bookmarkPlugin } from './store/bookmarks' import { registationPlugin } from './store/registation'; +import { siteLookupPlugin } from './store/websiteLookup'; //Setup the vnlib api configureApi({ @@ -67,9 +68,10 @@ store.use(profilePlugin('/account/profile')) //Enable mfa with totp settings plugin (optional pki config) .use(mfaSettingsPlugin('/account/mfa', '/account/pki')) //Setup social mfa plugin - .use(socialMfaPlugin()) + .use(socialMfaPlugin("/account/social/portals")) //Add the oauth2 apps plugin .use(bookmarkPlugin('/bookmarks')) + .use(siteLookupPlugin('/lookup', 2000)) .use(registationPlugin('/register')) //Setup oauth apps plugin (disabled for now) //.use(oauth2AppsPlugin('/oauth/apps', '/oauth/scopes')) diff --git a/front-end/src/store/bookmarks.ts b/front-end/src/store/bookmarks.ts index 2af8344..2d12a9a 100644 --- a/front-end/src/store/bookmarks.ts +++ b/front-end/src/store/bookmarks.ts @@ -18,7 +18,7 @@ import { MaybeRef, shallowRef, watch, computed, Ref, ref } from 'vue'; import { apiCall, useAxios, WebMessage } from '@vnuge/vnlib.browser'; import { useToggle, get, set, useOffsetPagination, watchDebounced, syncRef } from '@vueuse/core'; import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia' -import { isArray, join, map, split, sortBy } from 'lodash-es'; +import { isArray, join, map, split, sortBy, filter, isEmpty } from 'lodash-es'; import { useQuery } from './index'; export interface Bookmark{ @@ -189,7 +189,7 @@ const searchQuery = (search: Ref<string | null>, tags: Ref<string[]>) => { const tagQuery = useQuery('t') const currentTags = computed({ - get: () => split(tagQuery.value, ' '), + get: () => filter(split(tagQuery.value, ' '), p => !isEmpty(p)), set: (value) => set(tagQuery, join(value, ' ')) }) diff --git a/front-end/src/store/socialMfaPlugin.ts b/front-end/src/store/socialMfaPlugin.ts index d8d7bb1..e0ec972 100644 --- a/front-end/src/store/socialMfaPlugin.ts +++ b/front-end/src/store/socialMfaPlugin.ts @@ -1,26 +1,19 @@ -// 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/>. import 'pinia' import { MaybeRef } from 'vue'; -import { useSocialOauthLogin, useUser, SocialOAuthPortal, fromPortals, useAxios } from '@vnuge/vnlib.browser' +import { + useUser, + useOauthLogin, + useSocialDefaultLogout, + fetchSocialPortals, + fromSocialPortals, + fromSocialConnections, +} from '@vnuge/vnlib.browser' import { get } from '@vueuse/core'; import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia' import { defer } from 'lodash-es'; -type SocialMfaPlugin = ReturnType<typeof useSocialOauthLogin> +type SocialMfaPlugin = ReturnType<typeof useOauthLogin> declare module 'pinia' { export interface PiniaCustomProperties { @@ -35,43 +28,40 @@ export const socialMfaPlugin = (portalEndpoint?: MaybeRef<string>): PiniaPlugin const { } = storeToRefs(store) const { logout } = useUser() - /** - * Override the logout function to default to a social logout, - * if the social logout fails, then we will logout the user - */ - const setLogoutMethod = (socialOauth: SocialMfaPlugin) => { - const logoutFunc = socialOauth.logout; + //Create social login from available portals + const defaultSocial = useSocialDefaultLogout( + useOauthLogin([]), + logout //fallback to default logout + ); - (socialOauth as any).logout = async () => { - if (await logoutFunc() === false) { - await logout() - } - } - } - - const _loadPromise = new Promise<SocialMfaPlugin>((resolve, reject) => { + const _loadPromise = new Promise<SocialMfaPlugin>((resolve, _) => { - if(get(portalEndpoint) == null) { - const socialOauth = useSocialOauthLogin([]) - setLogoutMethod(socialOauth) - return resolve(socialOauth) + if (get(portalEndpoint) == null) { + return resolve(defaultSocial) } + /* + Try to load social methods from server, if it fails, then we will + fall back to default + */ + defer(async () => { + try { - //Get axios instance - const axios = useAxios(null) - //Get all enabled portals - const { data } = await axios.get<SocialOAuthPortal[]>(get(portalEndpoint)!); - //Setup social providers from server portals - const socialOauth = useSocialOauthLogin(fromPortals(data)); - setLogoutMethod(socialOauth); + const portals = await fetchSocialPortals(get(portalEndpoint)!); + const social = fromSocialPortals(portals); + const methods = fromSocialConnections(social); + + //Create social login from available portals + const login = useOauthLogin(methods); + const socialOauth = useSocialDefaultLogout(login, logout); resolve(socialOauth) } catch (error) { - reject(error) + //Let failure fall back to default + resolve(defaultSocial) } }) }) diff --git a/front-end/src/store/websiteLookup.ts b/front-end/src/store/websiteLookup.ts new file mode 100644 index 0000000..560d00f --- /dev/null +++ b/front-end/src/store/websiteLookup.ts @@ -0,0 +1,76 @@ + +import 'pinia' +import { MaybeRef, Ref, shallowRef, watch } from 'vue'; +import { WebMessage, apiCall, useAxios } from '@vnuge/vnlib.browser' +import { get, set } from '@vueuse/core'; +import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia' +import { defer, filter, isEmpty, noop } from 'lodash-es'; + +export interface WebsiteLookupResult { + readonly title: string | undefined, + readonly description: string | undefined, + keywords: string[] | undefined, +} + +export interface LookupApi{ + isSupported: Ref<boolean>, + timeout: Ref<number>, + execLookup(url:string): Promise<WebsiteLookupResult> +} + +declare module 'pinia' { + export interface PiniaCustomProperties { + websiteLookup:{ + isSupported: boolean, + execLookup(url: string): Promise<WebsiteLookupResult> + } + } +} + +const urlToBase64UrlEncoded = (url: string) => { + return btoa(url) + .replace(/-/g, '+') + .replace(/_/g, '/') + .replace(/\./g, '=') //Fix padding +} + +export const siteLookupPlugin = (lookupEndpoint: MaybeRef<string>, to: number): PiniaPlugin => { + + return ({ store }: PiniaPluginContext) => { + + const { loggedIn } = storeToRefs(store) + const axios = useAxios(null) + + const isSupported = shallowRef(false) + const timeout = shallowRef(to) + + const checkIsSupported = () => { + return apiCall(async () => { + //Execute test with the 'support' query parameter + const { data } = await axios.get<WebMessage>(`${get(lookupEndpoint)}?support`) + set(isSupported, data.success) + }); + } + + const execLookup = async (url:string) => { + const base64Url = urlToBase64UrlEncoded(url) + + //Execute test with the 'support' query parameter + const { data } = await axios.get<WebMessage<WebsiteLookupResult>>(`${get(lookupEndpoint)}?timeout=${get(timeout)}&url=${base64Url}`) + const lookup = data.getResultOrThrow(); + lookup.keywords = filter(lookup.keywords, (k) => !isEmpty(k)) + return lookup + } + + //If login status changes, recheck support + watch([loggedIn], ([li]) => li ? defer(checkIsSupported) : noop(), { immediate: true }) + + return { + websiteLookup: { + isSupported, + execLookup, + timeout + } as LookupApi + } as any + } +}
\ No newline at end of file |