diff options
Diffstat (limited to 'cmnext-cli/src/Commands')
-rw-r--r-- | cmnext-cli/src/Commands/AuthnticationCommands.cs | 166 | ||||
-rw-r--r-- | cmnext-cli/src/Commands/ChannelCommands.cs | 371 | ||||
-rw-r--r-- | cmnext-cli/src/Commands/ConfigCommands.cs | 176 |
3 files changed, 713 insertions, 0 deletions
diff --git a/cmnext-cli/src/Commands/AuthnticationCommands.cs b/cmnext-cli/src/Commands/AuthnticationCommands.cs new file mode 100644 index 0000000..5e67594 --- /dev/null +++ b/cmnext-cli/src/Commands/AuthnticationCommands.cs @@ -0,0 +1,166 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Package: CMNext.Cli +* File: Program.cs +* +* CMNext.Cli is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* CMNext.Cli 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with CMNext.Cli. If not, see http://www.gnu.org/licenses/. +*/ + +using CMNext.Cli.Security; +using CMNext.Cli.Site; + +using System; +using System.Security.Authentication; +using System.Threading; +using System.Threading.Tasks; + +using Typin.Attributes; +using Typin.Console; +using Typin.Exceptions; + + +namespace CMNext.Cli.Commands +{ + public sealed class AuthnticationCommands + { + + [Command("auth", Description = "Manages your local authentication data")] + public sealed class AuthCommand : BaseCommand + { } + + [Command("auth login", Description = "Authenticates this client against your CMNext server")] + public sealed class AuthLoginCommand(SiteManager siteManager, VauthRunner vauth, ConsoleLogProvider logger) : BaseCommand + { + [CommandOption("stdin", 's', Description = "Reads the token from stdin instead of using vauth")] + public bool FromStdin { get; set; } + + [CommandOption("force", 'f', Description = "Forces a login even if you already have a valid authorization")] + public bool Force { get; set; } + + ///<inheritdoc/> + public override async ValueTask ExecuteAsync(IConsole console) + { + logger.SetVerbose(Verbose); + + //global cancel + CancellationToken cancellation = console.GetCancellationToken(); + + IPkiCredential token; + + //See if current auth is valid + if(await siteManager.HasValidAuth()) + { + if (!Force) + { + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("You already have a valid authorization!")); + return; + } + } + + if (FromStdin) + { + console.Output.WriteLine("Please enter your PKI token:"); + + //Read the token in from stdin + string? otp = await console.Input.ReadLineAsync(cancellation); + + if (string.IsNullOrWhiteSpace(otp)) + { + throw new CommandException("You must enter a one time login token to continue"); + } + + token = new PkiToken(otp); + } + else + { + console.Output.WriteLine("Getting token from vauth"); + //Get the token from vauth + token = await vauth.GetOptTokenAsync(); + } + + + console.Output.WriteLine("Logging in..."); + + try + { + await siteManager.AuthenticateAsync(token); + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("Login successful!")); + } + catch (AuthenticationException ae) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Output.WriteLine($"Authentication failed: {ae.Message}")); + } + finally + { + await siteManager.SaveStateAsync(); + } + } + + sealed record class PkiToken(string Token) : IPkiCredential + { + public string GetToken() => Token; + } + } + + [Command("auth logout", Description = "Destroys any local previous login state")] + public sealed class AuthLogoutCommand(SiteManager siteManager, ConsoleLogProvider logger) : BaseCommand + { + ///<inheritdoc/> + public override async ValueTask ExecuteAsync(IConsole console) + { + logger.SetVerbose(Verbose); + + console.Output.WriteLine("Logging out..."); + + try + { + //See if current auth is valid + await siteManager.LogoutAsync(); + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("Logout successful!")); + } + finally + { + await siteManager.SaveStateAsync(); + } + } + + sealed record class PkiToken(string Token) : IPkiCredential + { + public string GetToken() => Token; + } + } + + [Command("auth status", Description = "Gets you client's current authorization status")] + public sealed class AuthStatusCommand(SiteManager siteManager) : BaseCommand + { + public override async ValueTask ExecuteAsync(IConsole console) + { + //global cancel + CancellationToken cancellation = console.GetCancellationToken(); + + console.Output.WriteLine("Checking login status..."); + + if (await siteManager.HasValidAuth()) + { + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("You have a valid authorization")); + } + else + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Output.WriteLine("You do not have a valid authorization")); + } + } + } + } +}
\ No newline at end of file diff --git a/cmnext-cli/src/Commands/ChannelCommands.cs b/cmnext-cli/src/Commands/ChannelCommands.cs new file mode 100644 index 0000000..f184b0d --- /dev/null +++ b/cmnext-cli/src/Commands/ChannelCommands.cs @@ -0,0 +1,371 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Package: CMNext.Cli +* File: Program.cs +* +* CMNext.Cli is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* CMNext.Cli 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with CMNext.Cli. If not, see http://www.gnu.org/licenses/. +*/ + +using CMNext.Cli.Exceptions; +using CMNext.Cli.Model.Channels; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Typin.Attributes; +using Typin.Console; +using Typin.Utilities; + + +namespace CMNext.Cli.Commands +{ + public sealed class ChannelCommands + { + [Command("channels", Description = "Performs operations against blog channels within your cms")] + public sealed class ChannelCommand : BaseCommand + { } + + [Command("channels list", Description = "Lists all blog channels within your cms")] + public sealed class ChannelList(ChannelStore store, ConsoleLogProvider logger) : ListCommand + { + public override string Id { get; set; } = string.Empty; + + [CommandOption("search", 's', Description = "Show only results for a channel by it's name")] + public string Search { get; set; } = string.Empty; + + public override async ValueTask ExecuteAsync(IConsole console) + { + CancellationToken cancellation = console.GetCancellationToken(); + logger.SetVerbose(Verbose); + + ChannelMeta[]? channels = await store.ListAsync(cancellation); + + if (channels is null) + { + console.Error.WriteLine("No channels found"); + return; + } + + if(!string.IsNullOrWhiteSpace(Search)) + { + channels = channels.Where(c => c.Name.Contains(Search, StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine($"Found {channels.Length} channels")); + + TableUtils.Write( + console.Output, + channels, + ["Id", "Name", "Path", "Index", "Content", "Feed"], + null, + c => c.Id, + c => c.Name, + c => c.Path, + c => $"/{c.IndexFileName}", + c => $"/{c.ContentDir}", + c => (c.Feed != null) ? "*" : "" + ); + } + } + + [Command("channels delete", Description = "Deletes a channel by it's id")] + public sealed class ChannelDelete(ChannelStore store, ConsoleLogProvider logger) : DeleteCommand + { + [CommandOption("channel", 'c', Description = "Specifies the channel to delete", IsRequired = true)] + public override string Id { get; set; } = string.Empty; + + public override async ValueTask ExecuteAsync(IConsole console) + { + CancellationToken cancellation = console.GetCancellationToken(); + logger.SetVerbose(Verbose); + try + { + await store.DeleteAsync(Id, cancellation); + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine($"Deleted channel {Id}")); + } + catch (EntityNotFoundException) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Error.WriteLine($"Channel {Id} does not exist")); + return; + } + } + } + + [Command("channels create", Description = "Creates a new channel")] + public sealed class ChannelCreateCommand(ChannelStore store, ConsoleLogProvider logger) : BaseCommand + { + [CommandOption("name", 'n', Description = "Specifies the name of the channel", IsRequired = true)] + public string Name { get; set; } = string.Empty; + + [CommandOption("path", 'p', Description = "Specifies the path of the channel", IsRequired = true)] + public string Path { get; set; } = string.Empty; + + [CommandOption("index", 'i', Description = "Specifies the index file name of the channel")] + public string Index { get; set; } = "index.json"; + + [CommandOption("content", 'c', Description = "Specifies the content directory of the channel", IsRequired = true)] + public string Content { get; set; } = string.Empty; + + public async override ValueTask ExecuteAsync(IConsole console) + { + CancellationToken cancellation = console.GetCancellationToken(); + logger.SetVerbose(Verbose); + + ChannelMeta channel = new () + { + Name = Name, + Path = Path, + IndexFileName = Index, + ContentDir = Content, + }; + + await store.CreateAsync(channel, cancellation); + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("New channel created successfully")); + } + } + + [Command("channels set", Description = "Updates a channel")] + public sealed class ChannelUpdateCommand(ChannelStore store, ConsoleLogProvider logger) : BaseCommand + { + [CommandOption("channel", 'c', Description = "Specifies the channel to update", IsRequired = true)] + public string Channel { get; set; } = string.Empty; + + [CommandOption("name", 'n', Description = "Specifies the name of the channel")] + public string Name { get; set; } = string.Empty; + + [CommandOption("index", 'i', Description = "Specifies the index file name of the channel")] + public string Index { get; set; } = string.Empty; + + [CommandOption("content", Description = "Specifies the content directory of the channel")] + public string Content { get; set; } = string.Empty; + + public async override ValueTask ExecuteAsync(IConsole console) + { + CancellationToken cancellation = console.GetCancellationToken(); + logger.SetVerbose(Verbose); + + ChannelMeta? channel = await store.GetAsync(Channel, cancellation); + + if (channel is null) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Error.WriteLine($"Channel {Channel} does not exist")); + return; + } + + console.Output.WriteLine($"Found channel {Channel}"); + + if (!string.IsNullOrWhiteSpace(Name)) + { + channel.Name = Name; + console.Output.WriteLine($"Setting channel name to {Name}"); + } + + if (!string.IsNullOrWhiteSpace(Index)) + { + channel.IndexFileName = Index; + console.Output.WriteLine($"Setting channel index file name to {Index}"); + } + + if (!string.IsNullOrWhiteSpace(Content)) + { + channel.ContentDir = Content; + console.Output.WriteLine($"Setting channel content directory to {Content}"); + } + + await store.UpdateAsync(channel, cancellation); + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("Channel updated successfully")); + } + } + + [Command("channels feed get", Description = "Gets the RSS feed data for a channel if its set")] + public sealed class ChannelFeedGetCommand(ChannelStore store, ConsoleLogProvider logger) : BaseCommand + { + [CommandOption("channel", 'c', Description = "Specifies the channel to get the feed for", IsRequired = true)] + public string Channel { get; set; } = string.Empty; + + public async override ValueTask ExecuteAsync(IConsole console) + { + CancellationToken cancellation = console.GetCancellationToken(); + logger.SetVerbose(Verbose); + + ChannelMeta? channel = await store.GetAsync(Channel, cancellation); + + if (channel is null) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Error.WriteLine($"Channel {Channel} does not exist")); + return; + } + + console.Output.WriteLine($"Found channel {Channel}"); + + if (channel.Feed is null) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Error.WriteLine($"Channel {Channel} does not have a feed")); + return; + } + + console.Output.WriteLine($"Found feed for channel {Channel}"); + + console.ForegroundColor = ConsoleColor.Gray; + console.Output.WriteLine($" Path: {channel.Feed.Path}"); + console.Output.WriteLine($" Description: {channel.Feed.Description}"); + console.Output.WriteLine($" Author: {channel.Feed.Author}"); + console.Output.WriteLine($" Contact: {channel.Feed.Contact}"); + console.Output.WriteLine($" Image: {channel.Feed.ImagePath}"); + console.Output.WriteLine($" Link: {channel.Feed.Url}"); + + console.ResetColor(); + } + } + + [Command("channels feed set", Description = "Sets the RSS feed data for a channel")] + public sealed class ChannelFeedSetCommand(ChannelStore store, ConsoleLogProvider logger) : BaseCommand + { + [CommandOption("channel", 'c', Description = "Specifies the channel to set the feed for", IsRequired = true)] + public string Channel { get; set; } = string.Empty; + + [CommandOption("path", 'p', Description = "Specifies the path of the feed")] + public string Path { get; set; } = string.Empty; + + [CommandOption("description", 'd', Description = "Specifies the description of the feed")] + public string Description { get; set; } = string.Empty; + + [CommandOption("author", 'a', Description = "Specifies the author of the feed")] + public string Author { get; set; } = string.Empty; + + [CommandOption("contact", Description = "Specifies the contact email address of the feed")] + public string Contact { get; set; } = string.Empty; + + [CommandOption("image", 'i', Description = "Specifies the image of the feed")] + public string Image { get; set; } = string.Empty; + + [CommandOption("link", 'l', Description = "Specifies the link of the feed")] + public string Link { get; set; } = string.Empty; + + public async override ValueTask ExecuteAsync(IConsole console) + { + CancellationToken cancellation = console.GetCancellationToken(); + logger.SetVerbose(Verbose); + + ChannelMeta? channel = await store.GetAsync(Channel, cancellation); + + if (channel is null) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Error.WriteLine($"Channel {Channel} does not exist")); + return; + } + + console.Output.WriteLine($"Found channel {Channel}"); + + bool modified = false; + + //Allow creating a new feed if the channel doesn't have one + channel.Feed ??= new ChannelFeed(); + + if (!string.IsNullOrWhiteSpace(Path)) + { + channel.Feed.Path = Path; + console.Output.WriteLine($"Setting feed path to {Path}"); + modified = true; + } + if (!string.IsNullOrWhiteSpace(Contact)) + { + channel.Feed.Contact = Contact; + console.Output.WriteLine($"Setting feed contact to {Contact}"); + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Description)) + { + channel.Feed.Description = Description; + console.Output.WriteLine($"Setting feed description to {Description}"); + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Author)) + { + channel.Feed.Author = Author; + console.Output.WriteLine($"Setting feed author to {Author}"); + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Image)) + { + channel.Feed.ImagePath = Image; + console.Output.WriteLine($"Setting feed image to {Image}"); + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Link)) + { + channel.Feed.Url = Link; + console.Output.WriteLine($"Setting feed link to {Link}"); + modified = true; + } + + if (!modified) + { + console.WithForegroundColor(ConsoleColor.Yellow, c => c.Error.WriteLine($"No changes made to channel {Channel}")); + return; + } + + await store.UpdateAsync(channel, cancellation); + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("Channel feed updated successfully")); + } + + [Command("channels feed delete", Description = "Deletes the RSS feed data for a channel")] + public sealed class ChannelFeedDeleteCommand(ChannelStore store, ConsoleLogProvider logger) : BaseCommand + { + [CommandOption("channel", 'c', Description = "Specifies the channel to delete the feed for", IsRequired = true)] + public string Channel { get; set; } = string.Empty; + + public async override ValueTask ExecuteAsync(IConsole console) + { + CancellationToken cancellation = console.GetCancellationToken(); + logger.SetVerbose(Verbose); + + ChannelMeta? channel = await store.GetAsync(Channel, cancellation); + + if (channel is null) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Error.WriteLine($"Channel {Channel} does not exist")); + return; + } + + console.Output.WriteLine($"Found channel {Channel}"); + + if (channel.Feed is null) + { + console.WithForegroundColor(ConsoleColor.Red, c => c.Error.WriteLine($"Channel {Channel} does not have a feed")); + return; + } + + console.Output.WriteLine($"Found feed for channel {Channel}"); + + channel.Feed = null; + + await store.UpdateAsync(channel, cancellation); + console.WithForegroundColor(ConsoleColor.Green, c => c.Output.WriteLine("Channel feed deleted successfully")); + } + } + + } + } + + + +}
\ No newline at end of file diff --git a/cmnext-cli/src/Commands/ConfigCommands.cs b/cmnext-cli/src/Commands/ConfigCommands.cs new file mode 100644 index 0000000..ff5c439 --- /dev/null +++ b/cmnext-cli/src/Commands/ConfigCommands.cs @@ -0,0 +1,176 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Package: CMNext.Cli +* File: Program.cs +* +* CMNext.Cli is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* CMNext.Cli 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with CMNext.Cli. If not, see http://www.gnu.org/licenses/. +*/ + +using CMNext.Cli.Settings; + +using System; +using System.Globalization; +using System.Threading.Tasks; + +using Typin.Attributes; +using Typin.Console; + + +namespace CMNext.Cli.Commands +{ + public sealed class ConfigCommands + { + + [Command("config", Description = "Manages this client's configuration data")] + public class ConfigBase : BaseCommand + { } + + [Command("config set", Description = "Sets the configuration of this client")] + public sealed class SetConfigCommand(AppSettings state) : BaseCommand + { + [CommandOption("url", 'u', Description = "The url of your cmnext server")] + public string? Url { get; set; } + + + [CommandOption("channels", 'c', Description = "The path from the base-url to the channels endpoint")] + public string? Channels { get; set; } + + [CommandOption("posts", Description = "The path from the base-url to the posts endpoint")] + public string? Posts { get; set; } + + [CommandOption("content", Description = "The path from the base-url to the auth endpoint")] + public string? Content { get; set; } + + [CommandOption("login", Description = "The path from the base-url to the pki login endpoint")] + public string? Login { get; set; } + + [CommandOption("vauth", Description = "The command to use to execute vauth to generate login passcode")] + public string? Vauth { get; set; } + + ///<inheritdoc/> + public override async ValueTask ExecuteAsync(IConsole console) + { + bool modified = false; + + //get site config + AppConfig siteConfig = await state.GetConfigAsync(); + + if (!string.IsNullOrWhiteSpace(Url)) + { + console.Output.WriteLine($"Setting url to {Url}..."); + siteConfig.BaseAddress = Url; + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Channels)) + { + console.Output.WriteLine($"Setting channel path to {Channels}..."); + siteConfig.Endpoints.ChannelPath = Channels; + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Posts)) + { + console.Output.WriteLine($"Setting post path to {Posts}..."); + siteConfig.Endpoints.PostPath = Posts; + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Content)) + { + console.Output.WriteLine($"Setting content path to {Content}..."); + siteConfig.Endpoints.ContentPath = Content; + modified = true; + } + + if (!string.IsNullOrWhiteSpace(Login)) + { + console.Output.WriteLine($"Setting pki endpoint path to {Login}..."); + siteConfig.Endpoints.LoginPath = Login; + modified = true; + } + + if(!string.IsNullOrWhiteSpace(Vauth)) + { + console.Output.WriteLine($"Setting vauth command to {Vauth}..."); + siteConfig.VauthCommands = Vauth; + modified = true; + } + + //Save config if modified + if (modified) + { + console.WithForegroundColor(ConsoleColor.Gray, io => io.Output.WriteLine("Writing configuration...")); + await state.SaveConfigAsync(siteConfig); + console.WithForegroundColor(ConsoleColor.Green, io => io.Output.WriteLine("Configuration saved.")); + } + else + { + console.Output.WriteLine("No changes to save."); + } + } + } + + [Command("config set endpoints", Description = "Sets the configuration of this client")] + public sealed class SetConfigEndpointsCommand(AppSettings state) : BaseCommand + { + + ///<inheritdoc/> + public override async ValueTask ExecuteAsync(IConsole console) + { + bool modified = false; + + //get site config + AppConfig siteConfig = await state.GetConfigAsync(); + + + + //Save config if modified + if (modified) + { + console.Output.WriteLine("Saving configuration..."); + await state.SaveConfigAsync(siteConfig); + console.Output.WriteLine("Configuration saved."); + } + else + { + console.Output.WriteLine("No changes to save."); + } + } + } + + [Command("config get", Description = "Gets the configuration of this client")] + public sealed class GetConfigCommand(AppSettings state) : BaseCommand + { + ///<inheritdoc/> + public override async ValueTask ExecuteAsync(IConsole console) + { + //get site config + AppConfig siteConfig = await state.GetConfigAsync(); + + console.WithForegroundColor(ConsoleColor.Gray, c => c.Output.WriteLine("Current configuration:")); + + console.Output.WriteLine("vauth: {0}", siteConfig.VauthCommands); + console.Output.WriteLine($"Url: {siteConfig.BaseAddress}"); + + console.Output.WriteLine($"Channels: {siteConfig.Endpoints.ChannelPath}"); + console.Output.WriteLine($"Posts: {siteConfig.Endpoints.PostPath}"); + console.Output.WriteLine($"Content: {siteConfig.Endpoints.ContentPath}"); + console.Output.WriteLine($"Login: {siteConfig.Endpoints.LoginPath}"); + + } + } + } +}
\ No newline at end of file |