From 2c28feacb5130ad557020043e02c65d2c29c4642 Mon Sep 17 00:00:00 2001 From: Pilzinsel64 Date: Wed, 27 Sep 2023 14:12:55 +0200 Subject: [PATCH] introduce a very simple Nextcloud API --- .../Extensions.cs | 37 +++++ .../Model/NextcloudLogin.cs | 39 ++++++ .../Model/UserBackendCapabilities.cs | 22 +++ .../Model/UserInfo.cs | 131 ++++++++++++++++++ .../Model/UserQuota.cs | 37 +++++ .../NextcloudClient.cs | 129 +++++++++++++++++ .../OCS/APIs/OcsApiBase.cs | 18 +++ .../OCS/APIs/OcsApiCloud.cs | 23 +++ .../OCS/APIs/OcsApiCore.cs | 23 +++ .../OCS/APIs/OcsApiLoginFlowV2.cs | 35 +++++ .../OCS/GetOcsApiAuthCredentailsEventArgs.cs | 15 ++ .../OCS/OcsApi.cs | 104 ++++++++++++++ .../OCS/OcsApiAuthCredentials.cs | 21 +++ .../OCS/OcsApiUrlPath.cs | 31 +++++ .../Responses/Cloud/OcsResponseDataUser.cs | 90 ++++++++++++ .../OCS/Responses/Cloud/OcsResponseUser.cs | 13 ++ .../LoginFlowV2/OcsResponseLoginFlowV2.cs | 40 ++++++ .../OcsResponseLoginFlowV2Credentials.cs | 30 ++++ .../OCS/Responses/OcsResponse.cs | 26 ++++ .../OCS/Responses/OcsResponseData.cs | 12 ++ .../OCS/Responses/OcsResponseMeta.cs | 21 +++ ...Networking.CloudProviders.Nextcloud.csproj | 19 +++ Pilz.sln | 12 +- 23 files changed, 927 insertions(+), 1 deletion(-) create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/Extensions.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/Model/NextcloudLogin.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/Model/UserBackendCapabilities.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/Model/UserInfo.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/Model/UserQuota.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/NextcloudClient.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiBase.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCloud.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCore.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiLoginFlowV2.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/GetOcsApiAuthCredentailsEventArgs.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApi.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiAuthCredentials.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiUrlPath.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseDataUser.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseUser.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2Credentials.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponse.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseData.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseMeta.cs create mode 100644 Pilz.Networking.CloudProviders.Nextcloud/Pilz.Networking.CloudProviders.Nextcloud.csproj diff --git a/Pilz.Networking.CloudProviders.Nextcloud/Extensions.cs b/Pilz.Networking.CloudProviders.Nextcloud/Extensions.cs new file mode 100644 index 0000000..24c928c --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/Extensions.cs @@ -0,0 +1,37 @@ +using Pilz.Networking.CloudProviders.Nextcloud.Model; +using Pilz.Networking.CloudProviders.Nextcloud.OCS; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud +{ + public static class Extensions + { + public static string ToBasicAuth(this OcsApiAuthCredentials? credentials) + { + if (credentials != null) + { + var creds = $"{credentials?.LoginName}:{credentials?.AppPassword}"; + var bytes = Encoding.UTF8.GetBytes(creds); + return "Basic " + Convert.ToBase64String(bytes); + } + + return string.Empty; + } + + public static OcsApiAuthCredentials? ToOcsApiAuthCredentials(this NextcloudLogin? login) + { + if (!string.IsNullOrEmpty(login?.LoginName) && !string.IsNullOrEmpty(login?.AppPassword)) + return new OcsApiAuthCredentials(login.LoginName, login.AppPassword); + return null; + } + + public static OcsApiUrlPath FillParameters(this OcsApiUrlPath path, params object?[] @params) + { + return (OcsApiUrlPath)string.Format(path, @params); + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/Model/NextcloudLogin.cs b/Pilz.Networking.CloudProviders.Nextcloud/Model/NextcloudLogin.cs new file mode 100644 index 0000000..c30ee61 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/Model/NextcloudLogin.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; +using Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.LoginFlowV2; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.Model +{ + public class NextcloudLogin + { + /// + /// The server url the login credentials are for. + /// + public string? Server { get; set; } + + /// + /// The login name (username or password) used for the login. + /// + public string? LoginName { get; set; } + + /// + /// The app password that has been generated. + /// + public string? AppPassword { get; set; } + + public NextcloudLogin() + { + } + + public NextcloudLogin(OcsResponseLoginFlowV2Credentials response) + { + Server = response.Server; + LoginName = response.LoginName; + AppPassword = response.AppPassword; + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/Model/UserBackendCapabilities.cs b/Pilz.Networking.CloudProviders.Nextcloud/Model/UserBackendCapabilities.cs new file mode 100644 index 0000000..b234ac4 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/Model/UserBackendCapabilities.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.Model +{ + public class UserBackendCapabilities + { + /// + /// Defines if the display name can be changed. + /// + public bool SetDisplayName { get; set; } + + /// + /// Defines if the password can be changed. + /// + public bool SetPassword { get; set; } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/Model/UserInfo.cs b/Pilz.Networking.CloudProviders.Nextcloud/Model/UserInfo.cs new file mode 100644 index 0000000..e46205a --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/Model/UserInfo.cs @@ -0,0 +1,131 @@ +using Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.Cloud; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.Model +{ + public class UserInfo + { + /// + /// Defines if the user is enabled. + /// + public bool Enabled { get; set; } + + /// + /// The location of the user's storage directory. + /// + public string? StorageLocation { get; set; } + + /// + /// The uniquie user id that infos are for. + /// + public string? ID { get; set; } + + /// + /// The last time when the user has logged in to its account. + /// + public DateTime LastLogin { get; set; } + + /// + /// The backend of the user. Common values are "Database" or "LDAP". + /// + public string? Backend { get; set; } + + /// + /// The Email address of the user. + /// + public string? Email { get; set; } + + /// + /// The displayname of the user. + /// + public string? Displayname { get; set; } + + /// + /// The displayname of the user. + /// + public string? Displayname2 { get; set; } + + /// + /// The phone number of the user. + /// + public string? Phone { get; set; } + + /// + /// The address of the user. + /// + public string? Address { get; set; } + + /// + /// The Website of the user. + /// + public string? Website { get; set; } + + /// + /// The twitter profile name of the user. + /// + public string? Twitter { get; set; } + + /// + /// Defines the groups the user is member of. + /// + public string[] Groups { get; set; } + + /// + /// The configured language of the user. + /// + public string? Language { get; set; } + + /// + /// The configured location of the user. + /// + public string? Locale { get; set; } + + /// + /// Quota informations for the user. + /// + public UserQuota Quota { get; } = new(); + + /// + /// Backend capabilities of the user. + /// + public UserBackendCapabilities BackendCapabilities { get; } = new(); + + public UserInfo(OcsResponseDataUser responseData) + { + Enabled = Convert.ToBoolean(responseData.Enabled); + StorageLocation = responseData.StorageLocation; + ID = responseData.ID; + LastLogin = Convert.ToDateTime(responseData.LastLogin); + Backend = responseData.Backend; + Email = responseData.Email; + Displayname = responseData.Displayname; + Displayname2 = responseData.Displayname2; + Phone = responseData.Phone; + Address = responseData.Address; + Website = responseData.Website; + Twitter = responseData.Twitter; + Groups = responseData.Groups ?? Array.Empty(); + Language = responseData.Language; + Locale = responseData.Locale; + + if (responseData.Quota != null) + { + Quota.Free = responseData.Quota.Free ?? 0; + Quota.Used = responseData.Quota.Used ?? 0; + Quota.Total = responseData.Quota.Total ?? 0; + Quota.Relative = responseData.Quota.Relative ?? 0.0F; + Quota.Quota = responseData.Quota.Quota ?? 0; + } + + if (responseData.BackendCapabilities != null) + { + BackendCapabilities.SetDisplayName = Convert.ToBoolean(responseData.BackendCapabilities.SetDisplayName); + BackendCapabilities.SetPassword = Convert.ToBoolean(responseData.BackendCapabilities.SetPassword); + } + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/Model/UserQuota.cs b/Pilz.Networking.CloudProviders.Nextcloud/Model/UserQuota.cs new file mode 100644 index 0000000..dcbef3c --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/Model/UserQuota.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.Model +{ + public class UserQuota + { + /// + /// Amount of free bytes left. + /// + public ulong Free { get; set; } + + /// + /// Amount of already used bytes. + /// + public ulong Used { get; set; } + + /// + /// Total amount of all bytes (free + used). + /// + public ulong Total { get; set; } + + /// + /// Relative amount of used quota in percent. + /// + public float Relative { get; set; } + + /// + /// Total amount of bytes available. + /// + public ulong Quota { get; set; } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/NextcloudClient.cs b/Pilz.Networking.CloudProviders.Nextcloud/NextcloudClient.cs new file mode 100644 index 0000000..874f68f --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/NextcloudClient.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Pilz.Networking.CloudProviders.Nextcloud; +using Pilz.Networking.CloudProviders.Nextcloud.Model; +using Pilz.Networking.CloudProviders.Nextcloud.OCS; +using Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.LoginFlowV2; + +namespace ConsoleApp9 +{ + public class NextcloudClient : IDisposable + { + private readonly OcsApi ocs = new(); + + public NextcloudLogin? CurrentLogin { get; private set; } + + public NextcloudClient() + { + ocs.GetOcsApiAuthCredentails += Ocs_GetOcsApiAuthCredentails; + } + + private void Ocs_GetOcsApiAuthCredentails(object sender, GetOcsApiAuthCredentailsEventArgs eventArgs) + { + if (sender == ocs) + eventArgs.Credentials = CurrentLogin.ToOcsApiAuthCredentials(); + } + + public async Task LoginAsync(NextcloudLogin login) + { + // Ensure we are logged out + await LogoutAsync(); + + // Temporary set user login + CurrentLogin = login; + + // Try get user info & check if user is enabled + var userInfo = await GetUserInfoAsync(); + var isValid = userInfo != null && userInfo.Enabled; + + // If invalid, reset login credentials + if (!isValid) + CurrentLogin = null; + + return userInfo; + } + + public async Task LoginAsync(string baseUrl, CancellationToken cancellationToken) + { + // Ensure we are logged out + await LogoutAsync(); + + // Init the login process + var initResponse = await ocs.LoginFlowV2.Init(baseUrl); + + if (!string.IsNullOrEmpty(initResponse?.LoginUrl) && initResponse.Poll != null) + { + // Open the browser + Process.Start(new ProcessStartInfo + { + FileName = initResponse.LoginUrl, + UseShellExecute = true + }); + + // Poll for credentails in intervals + OcsResponseLoginFlowV2Credentials? pollResponse = null; + while (!cancellationToken.IsCancellationRequested && pollResponse is null) + { + // Wait 5 seconds + Thread.Sleep(5000); + + // Poll the credentials + if (!cancellationToken.IsCancellationRequested) + pollResponse = await ocs.LoginFlowV2.Poll(initResponse.Poll); + } + + // Check login credentials + if (pollResponse is not null) + CurrentLogin = new(pollResponse); + } + + return CurrentLogin; + } + + public Task GetUserInfoAsync() + { + if (!string.IsNullOrEmpty(CurrentLogin?.LoginName)) + return GetUserInfoAsync(CurrentLogin.LoginName); + else + return Task.FromResult(null); + } + + public async Task GetUserInfoAsync(string username) + { + var result = await ocs.Cloud.GetUserMeta(username); + + if (result?.Data != null) + return new UserInfo(result.Data); + + return null; + } + + public Task LogoutAsync() + { + return LogoutAsync(true); + } + + public async Task LogoutAsync(bool logoutOnServer) + { + if (CurrentLogin != null) + { + // Delete currently used app password + await ocs.Core.DeleteAppPassword(); + + // Reset current login infos + CurrentLogin = null; + } + } + + public void Dispose() + { + ocs.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiBase.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiBase.cs new file mode 100644 index 0000000..a0ec4ed --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiBase.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.APIs +{ + public abstract class OcsApiBase + { + public OcsApi Manager { get; init; } + + protected OcsApiBase(OcsApi manager) + { + Manager = manager; + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCloud.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCloud.cs new file mode 100644 index 0000000..21e4d95 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCloud.cs @@ -0,0 +1,23 @@ +using Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.Cloud; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.APIs +{ + public class OcsApiCloud : OcsApiBase + { + public readonly static OcsApiUrlPath OCS_CLOUD_USER_METADATA = new("/ocs/v1.php/cloud/users/{0}"); + + public OcsApiCloud(OcsApi manager) : base(manager) + { + } + + public Task GetUserMeta(string username) + { + return Manager.MakeRequestJson(HttpMethod.Get, OCS_CLOUD_USER_METADATA.FillParameters(username)); + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCore.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCore.cs new file mode 100644 index 0000000..2c53cbe --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiCore.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.APIs +{ + public class OcsApiCore : OcsApiBase + { + public static readonly OcsApiUrlPath OCS_CORE_APPPASSWORD = "/ocs/v2.php/core/apppassword"; + + public OcsApiCore(OcsApi manager) : base(manager) + { + } + + public async Task DeleteAppPassword() + { + using var msg = await Manager.MakeRequest(HttpMethod.Delete, OCS_CORE_APPPASSWORD); + return msg != null && msg.IsSuccessStatusCode; + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiLoginFlowV2.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiLoginFlowV2.cs new file mode 100644 index 0000000..357aeaa --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/APIs/OcsApiLoginFlowV2.cs @@ -0,0 +1,35 @@ +using Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.LoginFlowV2; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.APIs +{ + public class OcsApiLoginFlowV2 : OcsApiBase + { + private const string OCS_LOGIN_INIT = "/index.php/login/v2"; + + public OcsApiLoginFlowV2(OcsApi manager) : base(manager) + { + } + + public Task Init(string url) + { + return Manager.MakeRequestJson(HttpMethod.Get, url + OCS_LOGIN_INIT); + } + + public Task Poll(OcsResponseLoginFlowV2.PollData poll) + { + ArgumentNullException.ThrowIfNull(poll?.Endpoint); + ArgumentNullException.ThrowIfNull(poll?.Token); + + return Manager.MakeRequestJson(HttpMethod.Get, poll.Endpoint, + parameters: new Dictionary + { + { "token", poll.Token } + }); + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/GetOcsApiAuthCredentailsEventArgs.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/GetOcsApiAuthCredentailsEventArgs.cs new file mode 100644 index 0000000..f7e4694 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/GetOcsApiAuthCredentailsEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS +{ + public delegate void GetOcsApiAuthCredentailsEventHandler(object sender, GetOcsApiAuthCredentailsEventArgs eventArgs); + + public class GetOcsApiAuthCredentailsEventArgs : EventArgs + { + public OcsApiAuthCredentials? Credentials { get; set; } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApi.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApi.cs new file mode 100644 index 0000000..c87179d --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApi.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Net.Http.Headers; +using Newtonsoft.Json; +using System.Diagnostics; +using System.Net; +using Pilz.Networking.CloudProviders.Nextcloud.OCS.APIs; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS +{ + public class OcsApi : IDisposable + { + public event GetOcsApiAuthCredentailsEventHandler? GetOcsApiAuthCredentails; + + private readonly HttpClient client = new(); + + public string BaseUrl { get; set; } = string.Empty; + + public OcsApiLoginFlowV2 LoginFlowV2 { get; init; } + public OcsApiCore Core { get; init; } + public OcsApiCloud Cloud { get; init; } + + public OcsApi() + { + LoginFlowV2 = new(this); + Core = new(this); + Cloud = new(this); + } + + public string BuildFullUrl(OcsApiUrlPath path) + { + return BaseUrl + path; + } + + public Task MakeRequestJson(HttpMethod httpMethod, OcsApiUrlPath url, bool useAuthentication = true, Dictionary? parameters = null) + { + return MakeRequestJson(httpMethod, BuildFullUrl(url), useAuthentication: useAuthentication, parameters: parameters); + } + + public Task MakeRequest(HttpMethod httpMethod, OcsApiUrlPath url, bool useAuthentication = true, Dictionary? parameters = null) + { + return MakeRequest(httpMethod, BuildFullUrl(url), useAuthentication: useAuthentication, parameters: parameters); + } + + public async Task MakeRequestJson(HttpMethod httpMethod, string url, bool useAuthentication = true, Dictionary? parameters = null) + { + using var responseInit = await MakeRequest(httpMethod, url, useAuthentication: useAuthentication, parameters: parameters); + + if (responseInit != null) + { + var bodyInit = await responseInit.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(bodyInit); + } + + return default; + } + + public async Task MakeRequest(HttpMethod httpMethod, string url, bool useAuthentication = true, Dictionary? parameters = null) + { + OcsApiAuthCredentials? authentication; + string @params; + + // Get authentication + if (useAuthentication) + { + var args = new GetOcsApiAuthCredentailsEventArgs(); + GetOcsApiAuthCredentails?.Invoke(this, args); + authentication = args.Credentials; + } + else + authentication = null; + + // Parse params + if (parameters != null) + @params = "?" + string.Join(",", parameters.Select(p => $"{p.Key}={p.Value}")); + else + @params = string.Empty; + + // Send request + var request = new HttpRequestMessage + { + Method = httpMethod ?? HttpMethod.Post, + RequestUri = new Uri(url + @params), + Headers = + { + { "Accept", "application/json" }, + { "OCS-APIREQUEST", "true" }, + { "Authorization", authentication.ToBasicAuth() } + }, + }; + + return await client.SendAsync(request); + } + + public void Dispose() + { + client.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiAuthCredentials.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiAuthCredentials.cs new file mode 100644 index 0000000..6c24b71 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiAuthCredentials.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS +{ + public struct OcsApiAuthCredentials + { + public string LoginName { get; set; } + public string AppPassword { get; set; } + + public OcsApiAuthCredentials(string loginName, string appPassword) + { + LoginName = loginName; + AppPassword = appPassword; + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiUrlPath.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiUrlPath.cs new file mode 100644 index 0000000..f32c375 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/OcsApiUrlPath.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS +{ + public readonly struct OcsApiUrlPath + { + private readonly string path; + + public OcsApiUrlPath() + { + path = string.Empty; + } + + public OcsApiUrlPath(string path) + { + this.path = path; + } + + public static implicit operator string(OcsApiUrlPath o) => o.path; + public static implicit operator OcsApiUrlPath(string o) => new(o); + + public override readonly string ToString() + { + return path; + } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseDataUser.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseDataUser.cs new file mode 100644 index 0000000..0000713 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseDataUser.cs @@ -0,0 +1,90 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.Cloud +{ + public class OcsResponseDataUser : OcsResponseData + { + public class ResponseQuota + { + [JsonProperty("free")] + public ulong? Free { get; set; } + + [JsonProperty("used")] + public ulong? Used { get; set; } + + [JsonProperty("total")] + public ulong? Total { get; set; } + + [JsonProperty("relative")] + public float? Relative { get; set; } + + [JsonProperty("quota")] + public ulong? Quota { get; set; } + } + + public class ResponseBackendCapabilities + { + [JsonProperty("setDisplayName")] + public int? SetDisplayName { get; set; } + + [JsonProperty("setPassword")] + public int? SetPassword { get; set; } + } + + [JsonProperty("enabled")] + public int? Enabled { get; set; } + + [JsonProperty("storageLocation")] + public string? StorageLocation { get; set; } + + [JsonProperty("id")] + public string? ID { get; set; } + + [JsonProperty("lastLogin")] + public ulong? LastLogin { get; set; } + + [JsonProperty("backend")] + public string? Backend { get; set; } + + [JsonProperty("quota")] + public ResponseQuota? Quota { get; set; } + + [JsonProperty("email")] + public string? Email { get; set; } + + [JsonProperty("displayname")] + public string? Displayname { get; set; } + + [JsonProperty("display-name")] + public string? Displayname2 { get; set; } + + [JsonProperty("phone")] + public string? Phone { get; set; } + + [JsonProperty("address")] + public string? Address { get; set; } + + [JsonProperty("website")] + public string? Website { get; set; } + + [JsonProperty("twitter")] + public string? Twitter { get; set; } + + [JsonProperty("groups")] + public string[]? Groups { get; set; } + + [JsonProperty("language")] + public string? Language { get; set; } + + [JsonProperty("locale")] + public string? Locale { get; set; } + + [JsonProperty("backendCapabilities")] + public ResponseBackendCapabilities? BackendCapabilities { get; set; } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseUser.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseUser.cs new file mode 100644 index 0000000..f3f3e74 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/Cloud/OcsResponseUser.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.Cloud +{ + public class OcsResponseUser : OcsResponse + { + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2.cs new file mode 100644 index 0000000..11c45ff --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.LoginFlowV2 +{ + public class OcsResponseLoginFlowV2 + { + public class PollData + { + /// + /// The login token that has been created for the login process. + /// It can be used to poll the login state. + /// + [JsonProperty("token")] + public string? Token { get; set; } + + /// + /// + /// + [JsonProperty("endpoint")] + public string? Endpoint { get; set; } + } + + /// + /// + /// + [JsonProperty("poll")] + public PollData? Poll { get; set; } + + /// + /// The temporary login url that should be used for login. + /// + [JsonProperty("login")] + public string? LoginUrl { get; set; } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2Credentials.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2Credentials.cs new file mode 100644 index 0000000..121bf8c --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/LoginFlowV2/OcsResponseLoginFlowV2Credentials.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses.LoginFlowV2 +{ + public class OcsResponseLoginFlowV2Credentials + { + /// + /// The server url the login credentials are for. + /// + [JsonProperty("server")] + public string? Server { get; set; } + + /// + /// The login name (username or password) used for the login. + /// + [JsonProperty("loginName")] + public string? LoginName { get; set; } + + /// + /// The app password that has been generated. + /// + [JsonProperty("appPassword")] + public string? AppPassword { get; set; } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponse.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponse.cs new file mode 100644 index 0000000..c21a6a4 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponse.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses +{ + public class OcsResponse where TMeta : OcsResponseMeta where TData : OcsResponseData + { + [JsonProperty("meta")] + public TMeta? Meta { get; set; } + + [JsonProperty("data")] + public TData? Data { get; set; } + } + + public class OcsResponse : OcsResponse where TData : OcsResponseData + { + } + + public class OcsResponse : OcsResponse + { + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseData.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseData.cs new file mode 100644 index 0000000..cc3a1ac --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseData.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses +{ + public class OcsResponseData + { + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseMeta.cs b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseMeta.cs new file mode 100644 index 0000000..049eeb7 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/OCS/Responses/OcsResponseMeta.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pilz.Networking.CloudProviders.Nextcloud.OCS.Responses +{ + public class OcsResponseMeta + { + [JsonProperty("status")] + public string? Status { get; set; } + + [JsonProperty("statuscode")] + public int? StatusCode { get; set; } + + [JsonProperty("message")] + public string? Message { get; set; } + } +} diff --git a/Pilz.Networking.CloudProviders.Nextcloud/Pilz.Networking.CloudProviders.Nextcloud.csproj b/Pilz.Networking.CloudProviders.Nextcloud/Pilz.Networking.CloudProviders.Nextcloud.csproj new file mode 100644 index 0000000..c07d969 --- /dev/null +++ b/Pilz.Networking.CloudProviders.Nextcloud/Pilz.Networking.CloudProviders.Nextcloud.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + True + 1.yyyy.Mdd.Hmm + 1.2023.927.1412 + + + + + + + diff --git a/Pilz.sln b/Pilz.sln index 20a2f6a..1f05bbb 100644 --- a/Pilz.sln +++ b/Pilz.sln @@ -33,7 +33,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pilz.Cryptography", "Pilz.C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pilz.UI.Telerik.SymbolFactory", "Pilz.UI.Telerik.SymbolFactory\Pilz.UI.Telerik.SymbolFactory.csproj", "{2B3B8161-29FF-4526-9082-4410AB5144A5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pilz.UI.Telerik", "Pilz.UI.Telerik\Pilz.UI.Telerik.csproj", "{DF674119-CC28-40AA-968F-1E23D184A491}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pilz.UI.Telerik", "Pilz.UI.Telerik\Pilz.UI.Telerik.csproj", "{DF674119-CC28-40AA-968F-1E23D184A491}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pilz.Networking.CloudProviders.Nextcloud", "Pilz.Networking.CloudProviders.Nextcloud\Pilz.Networking.CloudProviders.Nextcloud.csproj", "{A91E966B-3A82-4F32-A703-2FC1C7654FD1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -171,6 +173,14 @@ Global {DF674119-CC28-40AA-968F-1E23D184A491}.Release|Any CPU.Build.0 = Release|Any CPU {DF674119-CC28-40AA-968F-1E23D184A491}.Release|x86.ActiveCfg = Release|Any CPU {DF674119-CC28-40AA-968F-1E23D184A491}.Release|x86.Build.0 = Release|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Debug|x86.ActiveCfg = Debug|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Debug|x86.Build.0 = Debug|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Release|Any CPU.Build.0 = Release|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Release|x86.ActiveCfg = Release|Any CPU + {A91E966B-3A82-4F32-A703-2FC1C7654FD1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE