diff options
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts.AppData')
4 files changed, 74 insertions, 34 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs index d6d936f..d39000b 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs @@ -23,7 +23,7 @@ */ using VNLib.Utils.Logging; -using VNLib.Plugins.Extensions.Loading.Routing; +using VNLib.Plugins.Extensions.Loading.Routing.Mvc; using VNLib.Plugins.Essentials.Accounts.AppData.Endpoints; diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs index 3c4f3e5..41b8b30 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs @@ -22,10 +22,11 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; using System.Net; using System.Linq; -using System.Collections.Generic; using System.Threading.Tasks; +using System.Collections.Generic; using VNLib.Net.Http; using VNLib.Hashing.Checksums; @@ -37,14 +38,16 @@ using VNLib.Plugins.Extensions.Loading.Routing; using VNLib.Plugins.Essentials.Accounts.AppData.Model; using VNLib.Plugins.Essentials.Accounts.AppData.Stores; +using VNLib.Plugins.Extensions.Loading.Routing.Mvc; +using static VNLib.Plugins.Essentials.Endpoints.ResourceEndpointBase; +using static VNLib.Plugins.Essentials.Accounts.AppData.Model.HttpExtensions; namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints { - - [EndpointPath("{{path}}")] + [EndpointLogName("Endpoint")] [ConfigurationName("web_endpoint")] - internal sealed class WebEndpoint(PluginBase plugin, IConfigScope config) : ProtectedWebEndpoint + internal sealed class WebEndpoint(PluginBase plugin, IConfigScope config) : IHttpController { const int DefaultMaxDataSize = 8 * 1024; @@ -52,19 +55,24 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints private readonly int MaxDataSize = config.GetValueOrDefault("max_data_size", DefaultMaxDataSize); private readonly string[] AllowedScopes = config.GetRequiredProperty<string[]>("allowed_scopes"); - protected async override ValueTask<VfReturnType> GetAsync(HttpEntity entity) + ///<inheritdoc/> + public ProtectionSettings GetProtectionSettings() => default; + + [HttpStaticRoute("{{path}}", HttpMethod.GET)] + [HttpRouteProtection(AuthorzationCheckLevel.Critical)] + public async ValueTask<VfReturnType> GetDataAsync(HttpEntity entity) { WebMessage webm = new(); - string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); - bool noCache = entity.QueryArgs.ContainsKey("no_cache"); + string? scopeId = GetScopeId(entity); + bool noCache = NoCacheQuery(entity); if (webm.Assert(scopeId != null, "Missing scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } - if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + if (webm.Assert(IsScopeAllowed(scopeId), "Invalid scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } @@ -72,26 +80,28 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints //If the connection has the no-cache header set, also bypass the cache noCache |= entity.Server.NoCache(); - //optionally bypass cache if the user requests it - RecordOpFlags flags = noCache ? RecordOpFlags.NoCache : RecordOpFlags.None; - - UserRecordData? record = await _store.GetRecordAsync(entity.Session.UserID, scopeId, flags, entity.EventCancellation); - - if (record is null) - { - return VirtualClose(entity, webm, HttpStatusCode.NotFound); - } + UserRecordData? record = await _store.GetRecordAsync( + entity.Session.UserID, + recordKey: scopeId, + flags: noCache ? RecordOpFlags.NoCache : RecordOpFlags.None, //optionally bypass cache if the user requests it + entity.EventCancellation + ); //return the raw data with the checksum header - entity.SetRecordResponse(record, HttpStatusCode.OK); - return VfReturnType.VirtualSkip; + + return record is null + ? VirtualClose(entity, webm, HttpStatusCode.NotFound) + : CloseWithRecord(entity, record, HttpStatusCode.OK); } - protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity) + + [HttpStaticRoute("{{path}}", HttpMethod.PUT)] + [HttpRouteProtection(AuthorzationCheckLevel.Critical)] + public async ValueTask<VfReturnType> UpdateDataAsync(HttpEntity entity) { WebMessage webm = new(); - string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); - bool flush = entity.QueryArgs.ContainsKey("flush"); + string? scopeId = GetScopeId(entity); + bool flush = NoCacheQuery(entity); if (webm.Assert(entity.Files.Count == 1, "Invalid file count")) { @@ -103,7 +113,7 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } - if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + if (webm.Assert(IsScopeAllowed(scopeId), "Invalid scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } @@ -125,7 +135,7 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints //Compute checksum on sent data and compare to the header if it exists ulong checksum = FNV1a.Compute64(recordData); - ulong? userChecksum = entity.Server.GetUserDataChecksum(); + ulong? userChecksum = GetUserDataChecksum(entity.Server); if (userChecksum.HasValue) { @@ -144,28 +154,54 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints RecordOpFlags flags = flush ? RecordOpFlags.WriteThrough : RecordOpFlags.None; //Write the record to the store - await _store.SetRecordAsync(entity.Session.UserID, scopeId, recordData, checksum, flags, entity.EventCancellation); + await _store.SetRecordAsync( + userId: entity.Session.UserID, + recordKey: scopeId, + recordData, + checksum, + flags, + entity.EventCancellation + ); + return VirtualClose(entity, HttpStatusCode.Accepted); } - protected override async ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) + [HttpStaticRoute("{{path}}", HttpMethod.DELETE)] + [HttpRouteProtection(AuthorzationCheckLevel.Critical)] + public async ValueTask<VfReturnType> DeleteDataAsync(HttpEntity entity) { WebMessage webm = new(); - string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); + string? scopeId = GetScopeId(entity); if (webm.Assert(scopeId != null, "Missing scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } - if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + if (webm.Assert(IsScopeAllowed(scopeId), "Invalid scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } //Write the record to the store - await _store.DeleteRecordAsync(entity.Session.UserID, scopeId, entity.EventCancellation); + await _store.DeleteRecordAsync( + userId: entity.Session.UserID, + recordKey: scopeId, + entity.EventCancellation + ); + return VirtualClose(entity, HttpStatusCode.Accepted); + } + + private bool IsScopeAllowed(string scopeId) + { + return AllowedScopes.Contains(scopeId, StringComparer.OrdinalIgnoreCase); } + + private static string? GetScopeId(HttpEntity entity) + => entity.QueryArgs.GetValueOrDefault("scope"); + + private static bool NoCacheQuery(HttpEntity entity) + => entity.QueryArgs.ContainsKey("no_cache"); } } diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs index 9628b79..c3b990c 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs @@ -23,6 +23,7 @@ */ using System; +using System.Collections.Generic; using System.Net; using VNLib.Net.Http; @@ -33,7 +34,7 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Model { const string ChecksumHeader = "X-Data-Checksum"; - public static void SetRecordResponse(this HttpEntity entity, UserRecordData record, HttpStatusCode code) + public static VfReturnType CloseWithRecord(HttpEntity entity, UserRecordData record, HttpStatusCode code) { //Set checksum header entity.Server.Headers.Append(ChecksumHeader, $"{record.Checksum}"); @@ -44,14 +45,16 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Model ContentType.Binary, new BinDataRecordReader(record.Data) ); + + return VfReturnType.VirtualSkip; } - public static ulong? GetUserDataChecksum(this IConnectionInfo server) + public static ulong? GetUserDataChecksum(IConnectionInfo server) { string? checksumStr = server.Headers[ChecksumHeader]; return string.IsNullOrWhiteSpace(checksumStr) && ulong.TryParse(checksumStr, out ulong checksum) ? checksum : null; } - + sealed class BinDataRecordReader(byte[] recordData) : IMemoryResponseReader { private int _read; diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs index 2347c66..3761f07 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs @@ -42,7 +42,8 @@ using VNLib.Plugins.Essentials.Accounts.AppData.Model; namespace VNLib.Plugins.Essentials.Accounts.AppData.Stores.Sql { - internal sealed class SqlBackingStore(PluginBase plugin) : IEntityStore<UserRecordData, AppDataRequest>, IAsyncConfigurable + internal sealed class SqlBackingStore(PluginBase plugin) + : IEntityStore<UserRecordData, AppDataRequest>, IAsyncConfigurable { private readonly DbRecordStore _store = new(plugin.GetContextOptionsAsync()); |