aboutsummaryrefslogtreecommitdiff
path: root/back-end/src/Endpoints/ChannelEndpoint.cs
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-07-12 01:28:23 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-07-12 01:28:23 -0400
commitf64955c69d91e578e580b409ba31ac4b3477da96 (patch)
tree16f01392ddf1abfea13d7d1ede3bfb0459fe8f0d /back-end/src/Endpoints/ChannelEndpoint.cs
Initial commit
Diffstat (limited to 'back-end/src/Endpoints/ChannelEndpoint.cs')
-rw-r--r--back-end/src/Endpoints/ChannelEndpoint.cs257
1 files changed, 257 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);
+ }
+ }
+ }
+}