diff options
Diffstat (limited to 'cmnext-cli/src/Site')
-rw-r--r-- | cmnext-cli/src/Site/CMNextEndpointDefintion.cs | 330 | ||||
-rw-r--r-- | cmnext-cli/src/Site/CMNextSiteAdapter.cs | 137 | ||||
-rw-r--r-- | cmnext-cli/src/Site/ChannelRequests.cs | 34 | ||||
-rw-r--r-- | cmnext-cli/src/Site/ContentRequests.cs | 38 | ||||
-rw-r--r-- | cmnext-cli/src/Site/PostRequests.cs | 34 | ||||
-rw-r--r-- | cmnext-cli/src/Site/SiteManager.cs | 173 |
6 files changed, 746 insertions, 0 deletions
diff --git a/cmnext-cli/src/Site/CMNextEndpointDefintion.cs b/cmnext-cli/src/Site/CMNextEndpointDefintion.cs new file mode 100644 index 0000000..6066779 --- /dev/null +++ b/cmnext-cli/src/Site/CMNextEndpointDefintion.cs @@ -0,0 +1,330 @@ +/* +* 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 RestSharp; + +using System; +using System.Net; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using VNLib.Utils.Extensions; +using VNLib.Utils.Logging; +using VNLib.Net.Rest.Client.Construction; + +using CMNext.Cli.Exceptions; +using CMNext.Cli.Security; +using System.Diagnostics; + +namespace CMNext.Cli.Site +{ + public interface ICMNextEndpointMap + { + string ChannelPath { get; } + + string PostPath { get; } + + string ContentPath { get; } + + string LoginPath { get; } + } + + + public sealed class CMNextEndpointDefintion(ICMNextEndpointMap Endpoints, IAuthAdapter Auth, ILogProvider Logger) : IRestEndpointDefinition + { + public void BuildRequest(IRestSiteAdapter site, IRestEndpointBuilder builder) + { + builder.WithEndpoint<ListChannelRequest>() + .WithUrl(Endpoints.ChannelPath) + .AcceptJson() + .WithMethod(Method.Get) + .WithAccessDeniedHandler("You do not have the required permissions to list channels. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<GetChannelRequest>() + .WithUrl(Endpoints.ChannelPath) + .AcceptJson() + .WithMethod(Method.Get) + .WithQuery("id", p => p.ChannelId) + .WithAccessDeniedHandler("You do not have the required permissions to get a channel. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<SetChannelRequest>() + .WithUrl(Endpoints.ChannelPath) + .AcceptJson() + .WithMethod(Method.Patch) + .WithBody(r => r.Channel) + .WithAccessDeniedHandler("You do not have the required permissions to update a channel. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<CreateChannelRequest>() + .WithUrl(Endpoints.ChannelPath) + .AcceptJson() + .WithMethod(Method.Post) + .WithBody(r => r.Channel) + .WithAccessDeniedHandler("You do not have the required permissions to create a channel. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<DeleteChannelRequest>() + .WithUrl(Endpoints.ChannelPath) + .AcceptJson() + .WithMethod(Method.Delete) + .WithQuery("channel", p => p.ChannelId) + .WithAccessDeniedHandler("You do not have the required permissions to delete a channel. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + //Setup post endpoints + builder.WithEndpoint<ListPostMetaRequest>() + .WithUrl(Endpoints.PostPath) + .AcceptJson() + .WithMethod(Method.Get) + .WithQuery("channel", p => p.ChannelId) + .WithAccessDeniedHandler("You do not have the required permissions to list all posts. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<GetPostMetaRequest>() + .WithUrl(Endpoints.PostPath) + .AcceptJson() + .WithMethod(Method.Get) + .WithQuery("channel", p => p.ChannelId) + .WithQuery("post", p => p.PostId) + .WithAccessDeniedHandler("You do not have the required permissions to get a post. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<SetPostMetaRequest>() + .WithUrl(Endpoints.PostPath) + .AcceptJson() + .WithMethod(Method.Patch) + .WithBody(r => r.Post) + .WithQuery("channel", p => p.ChannelId) + .WithAccessDeniedHandler("You do not have the required permissions to modify a post. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<CreatePostMetaRequest>() + .WithUrl(Endpoints.PostPath) + .AcceptJson() + .WithMethod(Method.Post) + .WithBody(r => r.Post) + .WithQuery("channel", p => p.ChannelId) + .WithAccessDeniedHandler("You do not have the required permissions to create a post. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<DeletePostMetaRequest>() + .WithUrl(Endpoints.PostPath) + .AcceptJson() + .WithMethod(Method.Delete) + .WithQuery("channel", p => p.ChannelId) + .WithQuery("post", p => p.PostId) + .WithAccessDeniedHandler("You do not have the required permissions to delete a post. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + //Setup content endpoints + builder.WithEndpoint<ListContentRequest>() + .WithUrl(Endpoints.ContentPath) + .AcceptJson() + .WithMethod(Method.Get) + .WithQuery("channel", p => p.ChannelId) + .WithAccessDeniedHandler("You do not have the required permissions to list all content. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<SetContentMetaRequest>() + .WithUrl(Endpoints.ContentPath) + .AcceptJson() + .WithMethod(Method.Patch) + .WithQuery("channel", p => p.ChannelId) + .WithBody(r => r.Content) + .WithAccessDeniedHandler("You do not have the required permissions to modify content metadata. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<GetContentLinkRequest>() + .WithUrl(Endpoints.ContentPath) + .AcceptJson() + .WithMethod(Method.Get) + .WithQuery("channel", p => p.ChannelId) + .WithQuery("id", p => p.ContentId) + .WithQuery("getlink", "true") + .WithAccessDeniedHandler("You do not have the required permissions to get content. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<DeleteContentRequest>() + .WithUrl(Endpoints.ContentPath) + .AcceptJson() + .WithMethod(Method.Delete) + .WithQuery("channel", p => p.ChannelId) + .WithQuery("id", p => p.ContentId) + .WithAccessDeniedHandler("You do not have the required permissions to delete content. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<DeleteBulkContentRequest>() + .WithUrl(Endpoints.ContentPath) + .AcceptJson() + .WithMethod(Method.Delete) + .WithQuery("channel", p => p.ChannelId) + .WithQuery("ids", p => string.Join(',', p.ContentIds)) + .WithAccessDeniedHandler("You do not have the required permissions to delete content. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + builder.WithEndpoint<UploadFileRequest>() + .WithUrl(Endpoints.ContentPath) + .AcceptJson() + .WithMethod(Method.Put) + .WithQuery("channel", p => p.ChannelId) + .WithQuery("id", p => p.ContentId!) //Allowed to be null, it will be ignored + .WithHeader("X-Content-Name", p => p.Name) + .WithModifier((r, req) => req.AddFile("file", r.LocalFile.FullName)) //Add the file from its fileinfo + .WithAccessDeniedHandler("You do not have the required permissions to upload content. Access denied") + .WithAuth(Auth) + .WithLogger(Logger); + + //Setup server poke endpoint + } + } + + + + internal static class EndpointExtensions + { + /// <summary> + /// Specifes that the desired response Content-Type is of application/json + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="builder"></param> + /// <returns></returns> + public static IRestRequestBuilder<T> AcceptJson<T>(this IRestRequestBuilder<T> builder) + { + return builder.WithHeader("Accept", "application/json"); + } + + public static IRestRequestBuilder<T> WithAccessDeniedHandler<T>(this IRestRequestBuilder<T> builder, string message) + { + return builder.OnResponse((_, res) => + { + if (res.StatusCode == HttpStatusCode.Forbidden) + { + throw new CMNextPermissionException(message); + } + }); + } + + public static IRestRequestBuilder<T> WithBody<T, TBody>(this IRestRequestBuilder<T> builder, Func<T, TBody> body) where TBody : class + { + return builder.WithModifier((t, req) => req.AddJsonBody(body(t))); + } + + public static IRestRequestBuilder<T> WithLogger<T>(this IRestRequestBuilder<T> builder, ILogProvider logger) + { + builder.WithModifier((t, req) => + { + Debug.Assert(req.CookieContainer != null); + string[] cookies = req.CookieContainer!.GetAllCookies().Select(c => $"{c.Name}={c.Value}").ToArray(); + string cookie = string.Join("\n", cookies); + + //List all headers + string[] headers = req.Parameters.Where(p => p.Type == ParameterType.HttpHeader) + .Select(p => $"{p.Name}: {p.Value}") + .ToArray(); + + string h = string.Join("\n", headers); + + logger.Verbose("Sending: {0} {1} HTTP/1.1\n{2}\n{3}\n{4}", req.Method, req.Resource, h, cookie, t); + }); + + builder.OnResponse((_, res) => + { + string[] cookies = res.Cookies!.Select(c => $"{c.Name}={c.Value}").ToArray(); + string cookie = string.Join("\n", cookies); + + //list response headers + string[]? headers = res.Headers?.Select(h => $"{h.Name}: {h.Value}").ToArray(); + string h = string.Join("\n", headers ?? []); + + + logger.Verbose("Received: {0} {1} {2} -> {3} bytes \n{4}\n{5}\n{6}", + res.Request.Resource, + (int)res.StatusCode, + res.StatusCode.ToString(), + res.RawBytes?.Length, + h, + cookie, + res.Content + ); + }); + + return builder; + } + + /// <summary> + /// Specifies the authentication adapter for the endpoint + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="buider"></param> + /// <param name="adapter">The auth adapter to set for the endpoint</param> + /// <returns></returns> + public static IRestRequestBuilder<T> WithAuth<T>(this IRestRequestBuilder<T> buider, IAuthAdapter adapter) + { + //Specify adapter for desired endpoint + adapter.SetModifiersForEndpoint(buider); + return buider; + } + + public static PendingRequest<T> BeginRequest<T>(this IRestSiteAdapter site, T request) + => new (site, request); + + public sealed class PendingRequest<T>(IRestSiteAdapter Adapter, T request) + { + + private readonly LinkedList<Action<T>> _beforeExecChain = new(); + + public PendingRequest<T> BeforeRequest(Action<T> beforeRequest) + { + _beforeExecChain.AddLast(beforeRequest); + return this; + } + + public Task<RestResponse> ExecAsync(CancellationToken cancellation) + { + _beforeExecChain.TryForeach(p => p.Invoke(request)); + return Adapter.ExecuteAsync(request, cancellation); + } + + public Task<RestResponse<TJson>> ExecAsync<TJson>(CancellationToken cancellation) + { + return Adapter.ExecuteAsync<T, TJson>(request, cancellation); + } + } + } +}
\ No newline at end of file diff --git a/cmnext-cli/src/Site/CMNextSiteAdapter.cs b/cmnext-cli/src/Site/CMNextSiteAdapter.cs new file mode 100644 index 0000000..8cb6a85 --- /dev/null +++ b/cmnext-cli/src/Site/CMNextSiteAdapter.cs @@ -0,0 +1,137 @@ +/* +* 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 RestSharp; + +using System; +using System.Net; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using VNLib.Net.Rest.Client; +using VNLib.Net.Rest.Client.Construction; + +using FluentValidation; +using FluentValidation.Results; + +using CMNext.Cli.Exceptions; +using CMNext.Cli.Settings; + +namespace CMNext.Cli.Site +{ + public sealed class CMNextSiteAdapter : RestSiteAdapterBase + { + protected override RestClientPool Pool { get; } + + public CMNextSiteAdapter(AppConfig config) + { + Uri baseUri = new(config.BaseAddress); + + RestClientOptions options = new(baseUri) + { + RemoteCertificateValidationCallback = (_, _, _, err) => true, + AutomaticDecompression = DecompressionMethods.All, + Encoding = System.Text.Encoding.UTF8, + ThrowOnAnyError = false, + ThrowOnDeserializationError = true, + FollowRedirects = false, + UserAgent = "vnuge/cmnext-cli", + }; + + Pool = new RestClientPool(2, options); + } + + ///<inheritdoc/> + public override Task WaitAsync(CancellationToken cancellation = default) => Task.CompletedTask; + + ///<inheritdoc/> + public override void OnResponse(RestResponse response) + { + //always see if a json web-message error was returned + ParseErrorAndThrow(response); + + switch (response.StatusCode) + { + case HttpStatusCode.InternalServerError: + throw new CMNextApiException("The server encountered an internal error"); + case HttpStatusCode.NotFound: + throw new EntityNotFoundException("The requested entity was not found"); + case HttpStatusCode.Forbidden: + throw new CMNextPermissionException("You do not have the required permissions to perform this action. Access Denied"); + case HttpStatusCode.Unauthorized: + throw new CMNextPermissionException("Your credentials are invalid or expired. Access Denied"); + case HttpStatusCode.Conflict: + throw new CMNextApiException("The requested action could not be completed due to a conflict"); + default: + response.ThrowIfError(); + break; + } + } + + private static void ParseErrorAndThrow(RestResponse response) + { + if (response.RawBytes == null || response.ContentType != "application/json") + { + return; + } + + using JsonDocument doc = JsonDocument.Parse(response.RawBytes); + + //Webmessage must be an object + if(doc.RootElement.ValueKind != JsonValueKind.Object) + { + return; + } + + //Check for validation errors and raise them + if(doc.RootElement.TryGetProperty("errors", out JsonElement errors)) + { + //Desserilize the errors into a validation failure + ValidationFailure[] err = errors.EnumerateArray() + .Select(e => e.Deserialize<ServerValidationJson>()!) + .Select(e => new ValidationFailure(e.PropertyName, e.ErrorMessage)) + .ToArray(); + + //Raise a fluent validation exception from the server results + throw new ValidationException(err); + } + + //Get result now, we don't know it's type yet + _ = doc.RootElement.TryGetProperty("result", out JsonElement result); + + if (doc.RootElement.TryGetProperty("success", out JsonElement success)) + { + //If the request was not successful, throw an exception, a result will be a string + if (!success.GetBoolean()) + { + throw new CMNextException(result.GetString()!); + } + } + } + + internal record ServerValidationJson( + [property: JsonPropertyName("property")] string? PropertyName, + [property: JsonPropertyName("message")] string? ErrorMessage + ); + } +}
\ No newline at end of file diff --git a/cmnext-cli/src/Site/ChannelRequests.cs b/cmnext-cli/src/Site/ChannelRequests.cs new file mode 100644 index 0000000..c004636 --- /dev/null +++ b/cmnext-cli/src/Site/ChannelRequests.cs @@ -0,0 +1,34 @@ +/* +* 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.Model.Channels; + +namespace CMNext.Cli.Site +{ + internal sealed class ListChannelRequest() { } + + internal sealed record class GetChannelRequest(string ChannelId); + + internal sealed record class SetChannelRequest(ChannelMeta Channel); + + internal sealed record class CreateChannelRequest(ChannelMeta Channel); + + internal sealed record class DeleteChannelRequest(string ChannelId); +}
\ No newline at end of file diff --git a/cmnext-cli/src/Site/ContentRequests.cs b/cmnext-cli/src/Site/ContentRequests.cs new file mode 100644 index 0000000..dffeda9 --- /dev/null +++ b/cmnext-cli/src/Site/ContentRequests.cs @@ -0,0 +1,38 @@ +/* +* 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 System.IO; + +using CMNext.Cli.Model.Content; + +namespace CMNext.Cli.Site +{ + sealed record class ListContentRequest(string ChannelId); + + sealed record class GetContentLinkRequest(string ChannelId, string ContentId); + + sealed record class DeleteContentRequest(string ChannelId, string ContentId); + + sealed record class DeleteBulkContentRequest(string ChannelId, string[] ContentIds); + + internal sealed record class UploadFileRequest(string ChannelId, string? ContentId, string Name, FileInfo LocalFile); + + internal sealed record class SetContentMetaRequest(string ChannelId, ContentMeta Content); +}
\ No newline at end of file diff --git a/cmnext-cli/src/Site/PostRequests.cs b/cmnext-cli/src/Site/PostRequests.cs new file mode 100644 index 0000000..0f935fa --- /dev/null +++ b/cmnext-cli/src/Site/PostRequests.cs @@ -0,0 +1,34 @@ +/* +* 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.Model.Posts; + +namespace CMNext.Cli.Site +{ + internal sealed record class GetPostMetaRequest(string ChannelId, string PostId); + + internal sealed record class ListPostMetaRequest(string ChannelId); + + internal sealed record class SetPostMetaRequest(string ChannelId, PostMeta Post); + + internal sealed record class CreatePostMetaRequest(string ChannelId, PostMeta Post); + + internal sealed record class DeletePostMetaRequest(string ChannelId, string PostId); +}
\ No newline at end of file diff --git a/cmnext-cli/src/Site/SiteManager.cs b/cmnext-cli/src/Site/SiteManager.cs new file mode 100644 index 0000000..18a71a6 --- /dev/null +++ b/cmnext-cli/src/Site/SiteManager.cs @@ -0,0 +1,173 @@ +/* +* 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 RestSharp; +using System.Threading; +using System.Threading.Tasks; +using System.Security.Authentication; + +using VNLib.Plugins; +using VNLib.Net.Rest.Client.Construction; + +using Typin.Console; + +using CMNext.Cli.Exceptions; +using CMNext.Cli.Settings; +using CMNext.Cli.Security; +using CMNext.Cli.Storage; + +namespace CMNext.Cli.Site +{ + public sealed class SiteManager(AppSettings state, PersistentDataStore data, ConsoleLogProvider logger, IConsole console) + { + readonly Task<WebAuthenticator> _lazyAuth = GetAuthenticatorAsync(state, data); + readonly CancellationToken cancellation = console.GetCancellationToken(); + + static async Task<WebAuthenticator> GetAuthenticatorAsync(AppSettings Config, PersistentDataStore Data) + { + //Load site configuration + AppConfig site = await Config.GetConfigAsync(); + + WebAuthenticator wa = new(new(site.BaseAddress)); + + //try to load the auth data from the store + _ = await Data.RestoreAsync(Program.AuthFileName, wa); + + //Return the authenticator regardless of success + return wa; + } + + public async Task<bool> HasValidAuth() => (await _lazyAuth).HasValidLogin(); + + /// <summary> + /// Gets the site adapter for the current site and configures + /// it + /// </summary> + /// <returns></returns> + public async Task<CMNextSiteAdapter> GetAdapterAsync() + { + //Get the site configuration + AppConfig config = await state.GetConfigAsync(); + WebAuthenticator man = await _lazyAuth; + + //Init endpoint routes + CMNextEndpointDefintion endpoints = new(config.Endpoints, man, logger); + + //Create a new site adapter and build the endpoints + CMNextSiteAdapter adapter = new(config); + adapter.BuildEndpoints(endpoints); + + //Set internal poke endpoint + adapter.DefineSingleEndpoint() + .WithEndpoint<ServerPokeRequest>() + .WithUrl("/") + .WithMethod(Method.Get) + .WithModifier((_, r) => r.CookieContainer = man.Cookies) + .WithLogger(logger); + + //Set login endpoint + adapter.DefineSingleEndpoint() + .WithEndpoint<LoginRequest>() + .WithUrl(config.Endpoints.LoginPath) + .WithMethod(Method.Post) + .WithBody(r => r) + .WithModifier((_, r) => r.CookieContainer = man.Cookies) + .WithLogger(logger); + + return adapter; + } + + /// <summary> + /// Attempts to authenticate the provided site with the provided credentials + /// </summary> + /// <param name="site">The cmnext site adapter to connect to</param> + /// <param name="auth">The authenticator to use for the connection</param> + /// <param name="token"></param> + /// <param name="cancellation"></param> + /// <returns></returns> + /// <exception cref="AuthenticationException"></exception> + public async Task AuthenticateAsync(IPkiCredential token) + { + //Wait for web auth + WebAuthenticator auth = await _lazyAuth; + CMNextSiteAdapter adapter = await GetAdapterAsync(); + + //Prepare new login credentials + ISecurityCredential cred = auth.PrepareLogin(); + + /* + * We must poke the server before we can send the login + * request to make sure we have a valid session cookie + * ready for an upgrade + */ + await PokeServerAsync(adapter); + + //Create a new login request + LoginRequest request = new(token.GetToken(), cred); + + //Send the login request + WebMessage response = (await adapter.ExecuteAsync(request, cancellation).AsJson<WebMessage>())!; + + //Check for success and throw result string if not + if (!response.Success) + { + throw new AuthenticationException(response.Result!.ToString()); + } + + //Finalize the login + auth.FinalizeLogin(cred, response); + } + + /// <summary> + /// Destroys the current authentication session + /// </summary> + /// <returns></returns> + public async Task LogoutAsync() + { + //Wait for web auth + WebAuthenticator auth = await _lazyAuth; + auth.Destroy(); + } + + private async Task PokeServerAsync(CMNextSiteAdapter site) + { + try + { + await site.BeginRequest(new ServerPokeRequest()) + .ExecAsync(cancellation); + } + catch (CMNextPermissionException) + { + //its okay if there was a permission exception during poke + } + } + + public async Task SaveStateAsync() + { + //Save the authenticator state + WebAuthenticator auth = await _lazyAuth; + await data.SaveAsync(Program.AuthFileName, auth); + } + + sealed record class ServerPokeRequest(); + + } +}
\ No newline at end of file |