diff options
Diffstat (limited to 'back-end/src/Endpoints')
-rw-r--r-- | back-end/src/Endpoints/ChannelEndpoint.cs | 257 | ||||
-rw-r--r-- | back-end/src/Endpoints/ContentEndpoint.cs | 360 | ||||
-rw-r--r-- | back-end/src/Endpoints/PostsEndpoint.cs | 271 |
3 files changed, 888 insertions, 0 deletions
diff --git a/back-end/src/Endpoints/ChannelEndpoint.cs b/back-end/src/Endpoints/ChannelEndpoint.cs new file mode 100644 index 0000000..c63d093 --- /dev/null +++ b/back-end/src/Endpoints/ChannelEndpoint.cs @@ -0,0 +1,257 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: CMNext +* Package: Content.Publishing.Blog.Admin +* File: ChannelEndpoint.cs +* +* CMNext 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. +* +* CMNext 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.Net; +using System.Threading.Tasks; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +using FluentValidation; + +using VNLib.Plugins; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Validation; + +using Content.Publishing.Blog.Admin.Model; + +namespace Content.Publishing.Blog.Admin.Endpoints +{ + [ConfigurationName("channel_endpoint")] + internal sealed class ChannelEndpoint : ProtectedWebEndpoint + { + private static readonly IValidator<ChannelRequest> ChannelValidator = ChannelRequest.GetValidator(); + private static readonly IValidator<FeedMeta> FeedValidator = FeedMeta.GetValidator(); + + private readonly IChannelContextManager ContentManager; + + + public ChannelEndpoint(PluginBase plugin, IConfigScope config) + { + string? path = config["path"].GetString(); + + InitPathAndLog(path, plugin.Log); + + ContentManager = plugin.GetOrCreateSingleton<ChannelManager>(); + } + + protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) + { + //Check user read-permissions + if (!entity.Session.CanRead()) + { + return VfReturnType.Forbidden; + } + + //Get the blog context list + object[] contexts = await ContentManager.GetAllContextsAsync(entity.EventCancellation); + + //Return the list to the client + entity.CloseResponseJson(HttpStatusCode.OK, contexts); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + //Check user write-permissions + if (webm.Assert(entity.Session.CanWrite() == true, "You do not have permission to add channels")) + { + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + //Get the blog context from the request body + ChannelRequest? channel = await entity.GetJsonFromFileAsync<ChannelRequest>(); + + if (webm.Assert(channel != null, "You must specify a new blog channel")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Validate the blog context + if (!ChannelValidator.Validate(channel, webm)) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Validate the feed if its defined + if (channel.Feed != null && !FeedValidator.Validate(channel.Feed, webm)) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Add the blog context to the manager + bool result = await ContentManager.CreateChannelAsync(channel, entity.EventCancellation); + + if (webm.Assert(result, "A blog with the given name already exists")) + { + entity.CloseResponseJson(HttpStatusCode.Conflict, webm); + return VfReturnType.VirtualSkip; + } + + //Return the new blog context to the client + entity.CloseResponse(HttpStatusCode.Created); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> PatchAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + //Check user write-permissions + if (webm.Assert(entity.Session.CanWrite() == true, "You do not have permission to add channels")) + { + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + //Get the blog context from the request body + ChannelRequest? channel = await entity.GetJsonFromFileAsync<ChannelRequest>(); + + if (webm.Assert(channel?.Id != null, "You must specify a new blog channel")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Validate the blog context + if (!ChannelValidator.Validate(channel, webm)) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Validate the feed if its defined + if (channel.Feed != null && !FeedValidator.Validate(channel.Feed, webm)) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Make sure the blog context exists + IChannelContext? context = await ContentManager.GetChannelAsync(channel.Id, entity.EventCancellation); + + if (webm.Assert(context != null, "The specified blog channel does not exist")) + { + entity.CloseResponseJson(HttpStatusCode.NotFound, webm); + return VfReturnType.VirtualSkip; + } + + //Update the context + bool result = await ContentManager.UpdateChannelAsync(channel, entity.EventCancellation); + + if (webm.Assert(result, "Failed to update the channel setting")) + { + entity.CloseResponseJson(HttpStatusCode.Conflict, webm); + return VfReturnType.VirtualSkip; + } + + //Return the new blog context to the client + entity.CloseResponse(HttpStatusCode.Created); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) + { + //Check for user write-permissions + if (!entity.Session.CanDelete()) + { + return VfReturnType.Forbidden; + } + + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? channelId)) + { + return VfReturnType.BadRequest; + } + + //Try to get the blog context from the id + IChannelContext? context = await ContentManager.GetChannelAsync(channelId, entity.EventCancellation); + + if (context == null) + { + return VfReturnType.NotFound; + } + + //Delete the blog context + await ContentManager.DeleteChannelAsync(context, entity.EventCancellation); + + //Return the new blog context to the client + entity.CloseResponse(HttpStatusCode.NoContent); + return VfReturnType.VirtualSkip; + } + + private sealed class ChannelRequest : BlogChannel, IJsonOnDeserialized + { + private static readonly Regex FileNameRegex = new(@"^[a-zA-Z0-9_\-.]+$", RegexOptions.Compiled); + private static readonly Regex DirectoryPathRegex = new(@"^[a-zA-Z0-9_\-/]+$", RegexOptions.Compiled); + + public static IValidator<ChannelRequest> GetValidator() + { + InlineValidator<ChannelRequest> validationRules = new (); + + validationRules.RuleFor(x => x.BlogName) + .NotEmpty() + .AlphaNumericOnly() + .MaximumLength(64); + + validationRules.RuleFor(x => x.BaseDir) + .NotEmpty() + .MaximumLength(100) + //Must not start with a forward slash + .Must(static p => !p.StartsWith('/') && !p.StartsWith('\\')) + .WithMessage("Channel directory must not start with a forward slash") + .Matches(DirectoryPathRegex) + .WithMessage("Channel directory must be a valid directory path"); + + validationRules.RuleFor(x => x.IndexPath) + .NotEmpty() + .MaximumLength(100) + .Must(static p => !p.StartsWith('/') && !p.StartsWith('\\')) + .WithMessage("Channel catalog file must not start with a forward slash") + //Must be a file path + .Matches(FileNameRegex) + .WithMessage("Channel catalog file path is not a valid file name"); + + validationRules.RuleFor(x => x.ContentDir) + .NotEmpty() + .Must(static p => !p.StartsWith('/') && !p.StartsWith('\\')) + .WithMessage("Channel content directory must not start with a forward slash") + .MaximumLength(100); + + return validationRules; + } + + public void OnDeserialized() + { + //Compute the uniqe id of the channel + Id = ChannelManager.ComputeContextId(this); + } + } + } +} diff --git a/back-end/src/Endpoints/ContentEndpoint.cs b/back-end/src/Endpoints/ContentEndpoint.cs new file mode 100644 index 0000000..e1e1344 --- /dev/null +++ b/back-end/src/Endpoints/ContentEndpoint.cs @@ -0,0 +1,360 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: CMNext +* Package: Content.Publishing.Blog.Admin +* File: ContentEndpoint.cs +* +* CMNext 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. +* +* CMNext 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.Net; +using System.Threading.Tasks; + +using FluentValidation; + +using VNLib.Utils.IO; +using VNLib.Net.Http; +using VNLib.Plugins; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Validation; + +using Content.Publishing.Blog.Admin.Model; + +namespace Content.Publishing.Blog.Admin.Endpoints +{ + + [ConfigurationName("content_endpoint")] + internal sealed class ContentEndpoint : ProtectedWebEndpoint + { + private static readonly IValidator<ContentMeta> MetaValidator = ContentMeta.GetValidator(); + + private readonly ContentManager _content; + private readonly IChannelContextManager _blogContextManager; + + private readonly int MaxContentLength; + + public ContentEndpoint(PluginBase plugin, IConfigScope config) + { + string? path = config["path"].GetString(); + + InitPathAndLog(path, plugin.Log); + + //Get the max content length + MaxContentLength = (int)config["max_content_length"].GetUInt32(); + + _content = plugin.GetOrCreateSingleton<ContentManager>(); + _blogContextManager = plugin.GetOrCreateSingleton<ChannelManager>(); + } + + + protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) + { + if (!entity.Session.CanRead()) + { + return VfReturnType.Forbidden; + } + + //Get the channel id + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? channelId)) + { + entity.CloseResponse(HttpStatusCode.BadRequest); + return VfReturnType.VirtualSkip; + } + + //Get the channel + IChannelContext? channel = await _blogContextManager.GetChannelAsync(channelId, entity.EventCancellation); + + if (channel == null) + { + entity.CloseResponse(HttpStatusCode.NotFound); + return VfReturnType.VirtualSkip; + } + + //Get the content id, if not set get all content meta items + if (!entity.QueryArgs.TryGetNonEmptyValue("id", out string? contentId)) + { + //Get all content items + ContentMeta[] items = await _content.GetAllContentItemsAsync(channel, entity.EventCancellation); + + //Return the items + entity.CloseResponseJson(HttpStatusCode.OK, items); + return VfReturnType.VirtualSkip; + + } + + //See if the user wants to get a link to the content + if (entity.QueryArgs.IsArgumentSet("getlink", "true")) + { + WebMessage webm = new() + { + //Get the content link + Result = await _content.GetExternalPathForItemAsync(channel, contentId, entity.EventCancellation) + }; + + //Set success if the link exists + webm.Success = webm.Result != null; + webm.Result ??= "The requested content item was not found in the database"; + + //Return the link + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + else + { + //Get content for single item + VnMemoryStream vms = new(); + try + { + //Get the content from the store + ContentMeta? meta = await _content.GetContentAsync(channel, contentId, vms, entity.EventCancellation); + + //it may not exist, cleanuup + if (meta?.ContentType == null) + { + vms.Dispose(); + entity.CloseResponse(HttpStatusCode.NotFound); + return VfReturnType.VirtualSkip; + } + else + { + //rewind the stream + vms.Seek(0, SeekOrigin.Begin); + + //Return the content + entity.CloseResponse(HttpStatusCode.OK, HttpHelpers.GetContentType(meta.ContentType), vms); + return VfReturnType.VirtualSkip; + } + } + catch + { + vms.Dispose(); + throw; + } + } + } + + /* + * Patch allows updating content meta data without having to upload the content again + */ + protected override async ValueTask<VfReturnType> PatchAsync(HttpEntity entity) + { + + //Get channel id + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? channelId)) + { + return VfReturnType.NotFound; + } + + ValErrWebMessage webm = new(); + + if (webm.Assert(entity.Session.CanWrite(), "You do not have permissions to update content")) + { + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + //Make sure there is content attached + if (webm.Assert(entity.Files.Count > 0, "No content was attached to the entity body")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Get the channel + IChannelContext? channel = await _blogContextManager.GetChannelAsync(channelId, entity.EventCancellation); + + if (webm.Assert(channel != null, "The channel does not exist")) + { + entity.CloseResponseJson(HttpStatusCode.NotFound, webm); + return VfReturnType.VirtualSkip; + } + + //Read meta from request + ContentMeta? requestedMeta = await entity.GetJsonFromFileAsync<ContentMeta>(); + + if (webm.Assert(requestedMeta?.Id != null, "You must supply a content id")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Validate the meta + if (!MetaValidator.Validate(requestedMeta, webm)) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Get the original content meta + ContentMeta? meta = await _content.GetMetaAsync(channel, requestedMeta.Id, entity.EventCancellation); + if (webm.Assert(meta != null, "The requested content item does not exist")) + { + entity.CloseResponseJson(HttpStatusCode.NotFound, webm); + return VfReturnType.VirtualSkip; + } + + //Currently only allow chaning the file name + meta.FileName = requestedMeta.FileName; + + //Set the meta item + await _content.SetMetaAsync(channel, meta, entity.EventCancellation); + + //Return the updated meta + webm.Result = meta; + webm.Success = true; + + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + /* + * Put adds or updates content + */ + protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity) + { + //Get channel id + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? channelId)) + { + return VfReturnType.NotFound; + } + + ValErrWebMessage webm = new(); + + if (webm.Assert(entity.Session.CanWrite(), "You do not have permissions to update content")) + { + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + //Make sure there is content attached + if (webm.Assert(entity.Files.Count > 0, "No content was attached to the entity body")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Check content length + if (webm.Assert(entity.Files[0].FileData.Length <= MaxContentLength, $"The content length is too long, max length is {MaxContentLength} bytes")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Get the first file + FileUpload file = entity.Files[0]; + + //Get the channel + IChannelContext? channel = await _blogContextManager.GetChannelAsync(channelId, entity.EventCancellation); + + if (webm.Assert(channel != null, "The channel does not exist")) + { + entity.CloseResponseJson(HttpStatusCode.NotFound, webm); + return VfReturnType.VirtualSkip; + } + + ContentMeta? meta; + + //Get the content id if its an update + if (entity.QueryArgs.TryGetNonEmptyValue("id", out string? contentId)) + { + //Get the original content meta + meta = await _content.GetMetaAsync(channel, contentId, entity.EventCancellation); + + if (webm.Assert(meta != null, "The request item does not exist")) + { + entity.CloseResponseJson(HttpStatusCode.NotFound, webm); + return VfReturnType.VirtualSkip; + } + + //May want to change the content name + meta.FileName = entity.Server.Headers["X-Content-Name"]; + } + else + { + //Get the content name, may be null + string? cName = entity.Server.Headers["X-Content-Name"]; + + //New item + meta = _content.GetNewMetaObject(file.FileData.Length, cName, file.ContentType); + } + + //Validate the meta after updating file name + if (!MetaValidator.Validate(meta, webm)) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Add or update the content + await _content.SetContentAsync(channel, meta, file.FileData, file.ContentType, entity.EventCancellation); + + //Return the meta + webm.Result = meta; + webm.Success = true; + + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) + { + if (!entity.Session.CanRead()) + { + return VfReturnType.Forbidden; + } + + //Get the channel id + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? channelId)) + { + entity.CloseResponse(HttpStatusCode.BadRequest); + return VfReturnType.VirtualSkip; + } + + //get the content id + if (!entity.QueryArgs.TryGetNonEmptyValue("id", out string? contentId)) + { + entity.CloseResponse(HttpStatusCode.BadRequest); + return VfReturnType.VirtualSkip; + } + + //Get channel + IChannelContext? channel = await _blogContextManager.GetChannelAsync(channelId, entity.EventCancellation); + if (channel == null) + { + return VfReturnType.NotFound; + } + + //Try to delete the content + bool deleted = await _content.DeleteContentAsync(channel, contentId, entity.EventCancellation); + + if (deleted) + { + entity.CloseResponse(HttpStatusCode.OK); + return VfReturnType.VirtualSkip; + } + else + { + return VfReturnType.NotFound; + } + } + } + +} diff --git a/back-end/src/Endpoints/PostsEndpoint.cs b/back-end/src/Endpoints/PostsEndpoint.cs new file mode 100644 index 0000000..fe7a310 --- /dev/null +++ b/back-end/src/Endpoints/PostsEndpoint.cs @@ -0,0 +1,271 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: CMNext +* Package: Content.Publishing.Blog.Admin +* File: PostsEndpoint.cs +* +* CMNext 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. +* +* CMNext 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.Threading.Tasks; +using Content.Publishing.Blog.Admin.Model; + +using FluentValidation; + +using VNLib.Plugins; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Validation; + +namespace Content.Publishing.Blog.Admin.Endpoints +{ + + [ConfigurationName("post_endpoint")] + internal sealed class PostsEndpoint : ProtectedWebEndpoint + { + private static readonly IValidator<BlogPost> PostValidator = BlogPost.GetValidator(); + + private readonly IBlogPostManager PostManager; + private readonly IChannelContextManager ContentManager; + + public PostsEndpoint(PluginBase plugin, IConfigScope config) + { + string? path = config["path"].GetString(); + + InitPathAndLog(path, plugin.Log); + + //Get post manager and context manager + PostManager = plugin.GetOrCreateSingleton<PostManager>(); + ContentManager = plugin.GetOrCreateSingleton<ChannelManager>(); + } + + protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) + { + //Check for read permissions + if (!entity.Session.CanRead()) + { + WebMessage webm = new() + { + Result = "You do not have permission to read content" + }; + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + //Try to get the blog id from the query + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? contextId)) + { + entity.CloseResponse(HttpStatusCode.BadRequest); + return VfReturnType.VirtualSkip; + } + + //Try to get the blog context from the id + IChannelContext? context = await ContentManager.GetChannelAsync(contextId, entity.EventCancellation); + if (context == null) + { + entity.CloseResponse(HttpStatusCode.NotFound); + return VfReturnType.VirtualSkip; + } + + //Try to get the post id from the query + if (entity.QueryArgs.TryGetNonEmptyValue("post", out string? postId)) + { + //Try to get single post + PostMeta? post = await PostManager.GetPostAsync(context, postId, entity.EventCancellation); + + if (post != null) + { + entity.CloseResponseJson(HttpStatusCode.OK, post); + } + else + { + entity.CloseResponse(HttpStatusCode.NotFound); + } + + return VfReturnType.VirtualSkip; + } + + //Get the post meta list + PostMeta[] posts = await PostManager.GetPostsAsync(context, entity.EventCancellation); + entity.CloseResponseJson(HttpStatusCode.OK, posts); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + //Check for write permissions + if (webm.Assert(entity.Session.CanWrite() == true, "You do not have permission to publish posts")) + { + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? contextId)) + { + webm.Result = "No blog channel was selected"; + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Try to get the blog context from the id + IChannelContext? context = await ContentManager.GetChannelAsync(contextId, entity.EventCancellation); + + if (webm.Assert(context != null, "A blog with the given id does not exist")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Get the post from the request body + BlogPost? post = await entity.GetJsonFromFileAsync<BlogPost>(); + + if (webm.Assert(post != null, "Message body was empty")) + { + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Validate post + if (!PostValidator.Validate(post, webm)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Publish post to the blog + await PostManager.PublishPostAsync(context, post, entity.EventCancellation); + + //Success + webm.Result = post; + webm.Success = true; + + //Return updated post to client + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> PatchAsync(HttpEntity entity) + { + ValErrWebMessage webm = new(); + + //Check for write permissions + if (webm.Assert(entity.Session.CanWrite() == true, "You do not have permissions to update posts")) + { + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + //Try to get the blog id from the query + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? contextId)) + { + webm.Result = "You must select a blog channel to update posts"; + entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); + return VfReturnType.VirtualSkip; + } + + //Try to get the blog context from the id + IChannelContext? channel = await ContentManager.GetChannelAsync(contextId, entity.EventCancellation); + + if (webm.Assert(channel != null, "The channel you selected does not exist")) + { + entity.CloseResponseJson(HttpStatusCode.NotFound, webm); + return VfReturnType.VirtualSkip; + } + + //Get the blog post object + BlogPost? post = await entity.GetJsonFromFileAsync<BlogPost>(); + + if (webm.Assert(post != null, "Message body was empty")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Validate post + if (!PostValidator.Validate(post, webm)) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Update post against manager + bool result = await PostManager.UpdatePostAsync(channel, post, entity.EventCancellation); + + if (webm.Assert(result, "Failed to update post because it does not exist or the blog channel was not found")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + //Success + webm.Result = post; + webm.Success = true; + + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + protected override async ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) + { + //Check for delete permissions + if (!entity.Session.CanDelete()) + { + WebMessage webm = new() + { + Result = "You do not have permission to delete content" + }; + entity.CloseResponseJson(HttpStatusCode.Forbidden, webm); + return VfReturnType.VirtualSkip; + } + + //Try to get the blog id from the query + if (!entity.QueryArgs.TryGetNonEmptyValue("channel", out string? contextId)) + { + entity.CloseResponse(HttpStatusCode.BadRequest); + return VfReturnType.VirtualSkip; + } + + //Try to get the blog context from the id + IChannelContext? context = await ContentManager.GetChannelAsync(contextId, entity.EventCancellation); + if (context == null) + { + entity.CloseResponse(HttpStatusCode.NotFound); + return VfReturnType.VirtualSkip; + } + + //Try to get the post id from the query + if (!entity.QueryArgs.TryGetNonEmptyValue("post", out string? postId)) + { + entity.CloseResponse(HttpStatusCode.NotFound); + return VfReturnType.VirtualSkip; + } + + //Delete post + await PostManager.DeletePostAsync(context, postId, entity.EventCancellation); + + //Success + entity.CloseResponse(HttpStatusCode.OK); + return VfReturnType.VirtualSkip; + } + + } +} |