Restructure project with namespace updates and renamed files for better organization. Migrate project name from FsTool to FsToolbox.Cli.
This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>FsToolbox.Cli</RootNamespace>
|
||||||
|
<AssemblyName>FsToolbox.Cli</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
namespace FsTool
|
using FsToolbox.Cli.Tasks;
|
||||||
|
|
||||||
|
namespace FsToolbox.Cli
|
||||||
{
|
{
|
||||||
public partial class CustomTasks
|
public partial class CustomTasks
|
||||||
{
|
{
|
||||||
|
#region Record AldiMember
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggregates store and member information for ALDI membership analysis.
|
/// Aggregates store and member information for ALDI membership analysis.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Store">The store participating in the membership relation.</param>
|
/// <param name="Store">The store participating in the membership relation.</param>
|
||||||
/// <param name="Member">The member associated with the store.</param>
|
/// <param name="Member">The member associated with the store.</param>
|
||||||
private record AldiMember(FsTool.Tasks.RegionTasks.Store Store, FsTool.Tasks.StoreTasks.Member Member);
|
private record AldiMember(RegionTasks.Store Store, StoreTasks.Member Member);
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
130
CustomTasks.cs
130
CustomTasks.cs
@@ -1,12 +1,73 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using FsTool.Helpers;
|
using FsToolbox.Cli.Helpers;
|
||||||
using FsTool.Tasks;
|
using FsToolbox.Cli.Tasks;
|
||||||
|
|
||||||
namespace FsTool
|
namespace FsToolbox.Cli
|
||||||
{
|
{
|
||||||
public partial class CustomTasks
|
public partial class CustomTasks
|
||||||
{
|
{
|
||||||
#region Public Method GetUnconfirmedPickupsLindenbergAsync
|
#region Public Method CheckAldiMembershipsAsync
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analyzes ALDI store memberships in the Siegen region and writes users with multiple memberships to a report file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to perform the requests.</param>
|
||||||
|
public static async Task CheckAldiMembershipsAsync(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
|
|
||||||
|
// Implementation for checking Aldi memberships would go here.
|
||||||
|
var stores = await RegionTasks.GetStoresInRegionAsync(httpClient, 139);
|
||||||
|
var activeAldis = stores.Where(x => x.CooperationStatus == RegionTasks.CooperationStatus.Cooperating && x.Name.Contains("ALDI", StringComparison.CurrentCultureIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
Console.WriteLine("Found " + activeAldis.Count + " active ALDI stores in region Siegen.");
|
||||||
|
|
||||||
|
var aldiMembers = new List<AldiMember>();
|
||||||
|
|
||||||
|
// Collect members from each active ALDI store
|
||||||
|
foreach (var activeAldi in activeAldis)
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
Console.WriteLine("Checking members for store: " + activeAldi.Name);
|
||||||
|
|
||||||
|
var members = await StoreTasks.GetStoreMembersAsync(httpClient, activeAldi.Id);
|
||||||
|
var activeMembers = members.Where(x => x is { Verified: StoreTasks.VerifiedStatus.Verified, Team_Active: StoreTasks.TeamActiveStatus.Active }).ToList();
|
||||||
|
|
||||||
|
activeMembers.ForEach(x => aldiMembers.Add(new(activeAldi, x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group memberships
|
||||||
|
var grouped = aldiMembers.GroupBy(x => x.Member.Id);
|
||||||
|
|
||||||
|
// Find groups with more than two memberships
|
||||||
|
var multipleMemberships = grouped.Where(g => g.Count() > 2).OrderByDescending(g => g.Count()).ToList();
|
||||||
|
|
||||||
|
|
||||||
|
Console.WriteLine($"Users with more than two ALDI store memberships: {multipleMemberships.Count}");
|
||||||
|
Console.WriteLine($"Saving data to file.");
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("ALDI Store Memberships Report");
|
||||||
|
sb.AppendLine("Generated: " + DateTime.Now.ToString("dd.MM.yyyy HH:mm"));
|
||||||
|
sb.AppendLine("============================");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
foreach (var group in multipleMemberships)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{group.First().Member.Name} ({group.Key}) - {group.Count()} ALDIs");
|
||||||
|
foreach (var membership in group) sb.AppendLine($" - {membership.Store.Name}");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file with timestamp
|
||||||
|
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
var filename = $"AldiMemberships_{timestamp}.txt";
|
||||||
|
await File.WriteAllTextAsync(filename, sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Method ConfirmUnconfirmedPickupsLindenbergAsync
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Confirms all unconfirmed pickup slots for the configured Lindenberg store after user confirmation.
|
/// Confirms all unconfirmed pickup slots for the configured Lindenberg store after user confirmation.
|
||||||
@@ -57,66 +118,5 @@ namespace FsTool
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analyzes ALDI store memberships in the Siegen region and writes users with multiple memberships to a report file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClient">The HTTP client used to perform the requests.</param>
|
|
||||||
public static async Task CheckAldiMembershipsAsync(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
|
||||||
|
|
||||||
// Implementation for checking Aldi memberships would go here.
|
|
||||||
var stores = await RegionTasks.GetStoresInRegionAsync(httpClient, 139);
|
|
||||||
var activeAldis = stores.Where(x => x.CooperationStatus == RegionTasks.CooperationStatus.Cooperating && x.Name.Contains("ALDI", StringComparison.CurrentCultureIgnoreCase)).ToList();
|
|
||||||
|
|
||||||
Console.WriteLine("Found " + activeAldis.Count + " active ALDI stores in region Siegen.");
|
|
||||||
|
|
||||||
var aldiMembers = new List<AldiMember>();
|
|
||||||
|
|
||||||
// Collect members from each active ALDI store
|
|
||||||
foreach (var activeAldi in activeAldis)
|
|
||||||
{
|
|
||||||
Thread.Sleep(200);
|
|
||||||
Console.WriteLine("Checking members for store: " + activeAldi.Name);
|
|
||||||
|
|
||||||
var members = await StoreTasks.GetStoreMembersAsync(httpClient, activeAldi.Id);
|
|
||||||
var activeMembers = members.Where(x => x is { Verified: StoreTasks.VerifiedStatus.Verified, Team_Active: StoreTasks.TeamActiveStatus.Active }).ToList();
|
|
||||||
|
|
||||||
activeMembers.ForEach(x => aldiMembers.Add(new(activeAldi, x)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group memberships
|
|
||||||
var grouped = aldiMembers.GroupBy(x => x.Member.Id);
|
|
||||||
|
|
||||||
// Find groups with more than two memberships
|
|
||||||
var multipleMemberships = grouped.Where(g => g.Count() > 2).OrderByDescending(g => g.Count()).ToList();
|
|
||||||
|
|
||||||
|
|
||||||
Console.WriteLine($"Users with more than two ALDI store memberships: {multipleMemberships.Count}");
|
|
||||||
Console.WriteLine($"Saving data to file.");
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine("ALDI Store Memberships Report");
|
|
||||||
sb.AppendLine("Generated: " + DateTime.Now.ToString("dd.MM.yyyy HH:mm"));
|
|
||||||
sb.AppendLine("============================");
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
foreach (var group in multipleMemberships)
|
|
||||||
{
|
|
||||||
sb.AppendLine($"{group.First().Member.Name} ({group.Key}) - {group.Count()} ALDIs");
|
|
||||||
foreach (var membership in group)
|
|
||||||
{
|
|
||||||
sb.AppendLine($" - {membership.Store.Name}");
|
|
||||||
}
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to file with timestamp
|
|
||||||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
|
||||||
var filename = $"AldiMemberships_{timestamp}.txt";
|
|
||||||
await File.WriteAllTextAsync(filename, sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
26
Endpoints.cs
26
Endpoints.cs
@@ -1,21 +1,29 @@
|
|||||||
using FsTool.Helpers;
|
using FsToolbox.Cli.Helpers;
|
||||||
|
|
||||||
namespace FsTool
|
namespace FsToolbox.Cli
|
||||||
{
|
{
|
||||||
public static class Endpoints
|
public static class Endpoints
|
||||||
{
|
{
|
||||||
private static string ApiBase => SettingsProvider.Current.Api.BaseUrl.TrimEnd('/');
|
#region Public Properties
|
||||||
|
|
||||||
public static string UserLogin => $"{ApiBase}/api/user/login";
|
public static string RegionStores => $"{ApiBase}/api/region/{{0}}/stores";
|
||||||
|
|
||||||
public static string UserCurrent => $"{ApiBase}/api/user/current";
|
|
||||||
|
|
||||||
public static string StorePickups => $"{ApiBase}/api/stores/{{0}}/pickups";
|
|
||||||
|
|
||||||
public static string StoreMembers => $"{ApiBase}/api/stores/{{0}}/member";
|
public static string StoreMembers => $"{ApiBase}/api/stores/{{0}}/member";
|
||||||
|
|
||||||
|
public static string StorePickups => $"{ApiBase}/api/stores/{{0}}/pickups";
|
||||||
|
|
||||||
public static string StorePickupsSlot => $"{ApiBase}/api/stores/{{0}}/pickups/{{1}}/{{2}}";
|
public static string StorePickupsSlot => $"{ApiBase}/api/stores/{{0}}/pickups/{{1}}/{{2}}";
|
||||||
|
|
||||||
public static string RegionStores => $"{ApiBase}/api/region/{{0}}/stores";
|
public static string UserCurrent => $"{ApiBase}/api/user/current";
|
||||||
|
|
||||||
|
public static string UserLogin => $"{ApiBase}/api/user/login";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Properties
|
||||||
|
|
||||||
|
private static string ApiBase => SettingsProvider.Current.Api.BaseUrl.TrimEnd('/');
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.5.2.0
|
VisualStudioVersion = 17.5.2.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FsTool", "FsTool.csproj", "{D8D12A22-90E0-56DB-D8E0-629AC330F42B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cli", "Cli.csproj", "{D8D12A22-90E0-56DB-D8E0-629AC330F42B}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -1,81 +1,100 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using FsTool.Tasks;
|
using FsToolbox.Cli.Tasks;
|
||||||
|
|
||||||
namespace FsTool.Helpers;
|
namespace FsToolbox.Cli.Helpers
|
||||||
|
|
||||||
public static class AuthHelper
|
|
||||||
{
|
{
|
||||||
private static async Task<string?> LoadCsrfTokenAsync()
|
/// <summary>
|
||||||
|
/// Provides helper methods to manage authentication and cached CSRF tokens.
|
||||||
|
/// </summary>
|
||||||
|
public static class AuthHelper
|
||||||
{
|
{
|
||||||
if (!File.Exists("csrf_token.json")) return null;
|
#region Public Method EnsureAuthenticationAsync
|
||||||
|
|
||||||
var json = await File.ReadAllTextAsync("csrf_token.json");
|
/// <summary>
|
||||||
var tokenInfo = JsonSerializer.Deserialize<JsonObject>(json);
|
/// Ensures the HTTP client carries a valid CSRF token, logging in when needed.
|
||||||
|
/// </summary>
|
||||||
if (tokenInfo == null) return null;
|
/// <param name="httpClient">The HTTP client whose headers should be updated.</param>
|
||||||
|
public static async Task EnsureAuthenticationAsync(HttpClient httpClient)
|
||||||
var token = tokenInfo["token"]?.GetValue<string>();
|
|
||||||
var expiresAtString = tokenInfo["expiresAt"]?.GetValue<string>();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(expiresAtString)) return null;
|
|
||||||
|
|
||||||
if (DateTime.TryParse(expiresAtString, out var expiresAt))
|
|
||||||
{
|
{
|
||||||
if (DateTime.UtcNow < expiresAt.ToUniversalTime())
|
// Check if header already contains a CSRF token
|
||||||
return token;
|
if (httpClient.DefaultRequestHeaders.TryGetValues("X-CSRF-Token", out var existingTokens))
|
||||||
|
{
|
||||||
|
var existingToken = existingTokens.FirstOrDefault();
|
||||||
|
if (!string.IsNullOrWhiteSpace(existingToken))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load CSRF token from file
|
||||||
|
var csrfToken = await LoadCsrfTokenAsync();
|
||||||
|
csrfToken = null;
|
||||||
|
|
||||||
|
// If no valid token found, call login endpoint to get a new one
|
||||||
|
if (string.IsNullOrWhiteSpace(csrfToken))
|
||||||
|
csrfToken = await UserTasks.CallLoginEndpointAsync(httpClient);
|
||||||
|
|
||||||
|
// Set CSRF token in HTTP client headers
|
||||||
|
if (!string.IsNullOrWhiteSpace(csrfToken))
|
||||||
|
{
|
||||||
|
csrfToken = csrfToken.ReplaceLineEndings(string.Empty);
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.Remove("X-CSRF-Token");
|
||||||
|
httpClient.DefaultRequestHeaders.Add("X-CSRF-Token", csrfToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
#endregion
|
||||||
}
|
|
||||||
|
|
||||||
internal static async Task StoreCsrfTokenAsync(string? csrfToken, DateTime expiration)
|
#region Public Method StoreCsrfTokenAsync
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(csrfToken)) return;
|
|
||||||
|
|
||||||
var tokenInfo = new
|
/// <summary>
|
||||||
|
/// Persists the CSRF token to the local cache file until it expires.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="csrfToken">The token to store; ignored when null or whitespace.</param>
|
||||||
|
/// <param name="expiration">The UTC expiration timestamp received from the server.</param>
|
||||||
|
public static async Task StoreCsrfTokenAsync(string? csrfToken, DateTime expiration)
|
||||||
{
|
{
|
||||||
token = csrfToken,
|
if (string.IsNullOrWhiteSpace(csrfToken)) return;
|
||||||
expiresAt = expiration
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(tokenInfo, new JsonSerializerOptions
|
var tokenInfo = new
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
token = csrfToken,
|
||||||
});
|
expiresAt = expiration
|
||||||
|
};
|
||||||
|
|
||||||
await File.WriteAllTextAsync("csrf_token.json", json);
|
var json = JsonSerializer.Serialize(tokenInfo, new JsonSerializerOptions
|
||||||
}
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
public static async Task EnsureAuthenticationAsync(HttpClient httpClient)
|
await File.WriteAllTextAsync("csrf_token.json", json);
|
||||||
{
|
|
||||||
// Check if header already contains a CSRF token
|
|
||||||
if (httpClient.DefaultRequestHeaders.TryGetValues("X-CSRF-Token", out var existingTokens))
|
|
||||||
{
|
|
||||||
var existingToken = existingTokens.FirstOrDefault();
|
|
||||||
if (!string.IsNullOrWhiteSpace(existingToken))
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load CSRF token from file
|
#endregion
|
||||||
var csrfToken = await LoadCsrfTokenAsync();
|
|
||||||
csrfToken = null;
|
|
||||||
|
|
||||||
// If no valid token found, call login endpoint to get a new one
|
#region Private Method LoadCsrfTokenAsync
|
||||||
if (string.IsNullOrWhiteSpace(csrfToken))
|
|
||||||
csrfToken = await UserTasks.CallLoginEndpointAsync(httpClient);
|
|
||||||
|
|
||||||
// Set CSRF token in HTTP client headers
|
private static async Task<string?> LoadCsrfTokenAsync()
|
||||||
if (!string.IsNullOrWhiteSpace(csrfToken))
|
|
||||||
{
|
{
|
||||||
csrfToken = csrfToken.ReplaceLineEndings(string.Empty);
|
if (!File.Exists("csrf_token.json")) return null;
|
||||||
|
|
||||||
httpClient.DefaultRequestHeaders.Remove("X-CSRF-Token");
|
var json = await File.ReadAllTextAsync("csrf_token.json");
|
||||||
httpClient.DefaultRequestHeaders.Add("X-CSRF-Token", csrfToken);
|
var tokenInfo = JsonSerializer.Deserialize<JsonObject>(json);
|
||||||
|
|
||||||
|
if (tokenInfo == null) return null;
|
||||||
|
|
||||||
|
var token = tokenInfo["token"]?.GetValue<string>();
|
||||||
|
var expiresAtString = tokenInfo["expiresAt"]?.GetValue<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(expiresAtString)) return null;
|
||||||
|
|
||||||
|
if (DateTime.TryParse(expiresAtString, out var expiresAt))
|
||||||
|
if (DateTime.UtcNow < expiresAt.ToUniversalTime())
|
||||||
|
return token;
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
namespace FsTool.Helpers
|
namespace FsToolbox.Cli.Helpers
|
||||||
{
|
{
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace FsTool.Helpers
|
namespace FsToolbox.Cli.Helpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the root configuration settings for the application, including API and credentials.
|
/// Represents the root configuration settings for the application, including API and credentials.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppSettings
|
public class AppSettings
|
||||||
{
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the API-related settings.
|
/// Gets the API-related settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -16,6 +18,8 @@ namespace FsTool.Helpers
|
|||||||
/// Gets the credentials settings for authentication.
|
/// Gets the credentials settings for authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CredentialsSettings Credentials { get; init; } = new();
|
public CredentialsSettings Credentials { get; init; } = new();
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -23,10 +27,14 @@ namespace FsTool.Helpers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApiSettings
|
public class ApiSettings
|
||||||
{
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the base URL for the API, without the /api path segment.
|
/// Gets the base URL for the API, without the /api path segment.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BaseUrl { get; init; } = "https://beta.foodsharing.de";
|
public string BaseUrl { get; init; } = "https://beta.foodsharing.de";
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -34,6 +42,8 @@ namespace FsTool.Helpers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CredentialsSettings
|
public class CredentialsSettings
|
||||||
{
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the email address used for login.
|
/// Gets the email address used for login.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -48,6 +58,8 @@ namespace FsTool.Helpers
|
|||||||
/// Gets a value indicating whether two-factor authentication is enabled.
|
/// Gets a value indicating whether two-factor authentication is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TwoFactorEnabled { get; init; }
|
public bool TwoFactorEnabled { get; init; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,23 +67,35 @@ namespace FsTool.Helpers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SettingsProvider
|
public static class SettingsProvider
|
||||||
{
|
{
|
||||||
|
#region Constants
|
||||||
|
|
||||||
private static AppSettings? _current;
|
private static AppSettings? _current;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current application settings instance.
|
/// Gets the current application settings instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static AppSettings Current => _current ??= Load();
|
public static AppSettings Current => _current ??= Load();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Method Load
|
||||||
|
|
||||||
private static AppSettings Load()
|
private static AppSettings Load()
|
||||||
{
|
{
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(AppContext.BaseDirectory)
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
.AddJsonFile("appsettings.json", false, true)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var settings = new AppSettings();
|
var settings = new AppSettings();
|
||||||
configuration.Bind(settings);
|
configuration.Bind(settings);
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using System.Net.Http.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FsTool;
|
using FsToolbox.Cli;
|
||||||
|
|
||||||
// See https://aka.ms/new-console-template for more information
|
// See https://aka.ms/new-console-template for more information
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
namespace FsTool.Tasks
|
namespace FsToolbox.Cli.Tasks
|
||||||
{
|
{
|
||||||
public partial class RegionTasks
|
public partial class RegionTasks
|
||||||
{
|
{
|
||||||
|
#region Record Store
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Basic region store information including cooperation status.
|
/// Basic region store information including cooperation status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -10,6 +12,10 @@ namespace FsTool.Tasks
|
|||||||
/// <param name="CooperationStatus">The cooperation status of the store.</param>
|
/// <param name="CooperationStatus">The cooperation status of the store.</param>
|
||||||
public record Store(int Id, string Name, CooperationStatus CooperationStatus);
|
public record Store(int Id, string Name, CooperationStatus CooperationStatus);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Enum CooperationStatus
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Describes the cooperation state between the store and the organization.
|
/// Describes the cooperation state between the store and the organization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -21,7 +27,9 @@ namespace FsTool.Tasks
|
|||||||
DoNotWant = 4,
|
DoNotWant = 4,
|
||||||
Cooperating = 5,
|
Cooperating = 5,
|
||||||
DonatingToTafel = 6,
|
DonatingToTafel = 6,
|
||||||
NoExisting = 7,
|
NoExisting = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using FsTool;
|
using FsToolbox.Cli.Helpers;
|
||||||
using FsTool.Helpers;
|
|
||||||
|
|
||||||
namespace FsTool.Tasks
|
namespace FsToolbox.Cli.Tasks
|
||||||
{
|
{
|
||||||
public partial class RegionTasks
|
public partial class RegionTasks
|
||||||
{
|
{
|
||||||
|
#region Public Method GetStoresInRegionAsync
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves all stores within the specified region, ensuring the request is authenticated.
|
/// Retrieves all stores within the specified region, ensuring the request is authenticated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -38,10 +39,12 @@ namespace FsTool.Tasks
|
|||||||
var opts = new JsonSerializerOptions
|
var opts = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
PropertyNamingPolicy = null // <── WICHTIG
|
PropertyNamingPolicy = null // <── WICHTIG
|
||||||
};
|
};
|
||||||
|
|
||||||
return root["stores"].Deserialize<List<Store>>(opts) ?? [];
|
return root["stores"].Deserialize<List<Store>>(opts) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,8 @@
|
|||||||
namespace FsTool.Tasks
|
namespace FsToolbox.Cli.Tasks
|
||||||
{
|
{
|
||||||
public static partial class StoreTasks
|
public static partial class StoreTasks
|
||||||
{
|
{
|
||||||
/// <summary>
|
#region Record Member
|
||||||
/// Represents a pickup date and the occupied slots for that date.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Date">The pickup date string.</param>
|
|
||||||
/// <param name="OccupiedSlots">The slots already assigned for the date.</param>
|
|
||||||
public record Pickup(string Date, List<Slot> OccupiedSlots);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Describes a booked slot and the profile occupying it.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="IsConfirmed">Indicates whether the slot is confirmed.</param>
|
|
||||||
/// <param name="Profile">The profile assigned to the slot.</param>
|
|
||||||
public record Slot(bool IsConfirmed, Profile Profile);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimal profile information for a store member.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Id">The Foodsharing profile identifier.</param>
|
|
||||||
/// <param name="Name">The profile display name.</param>
|
|
||||||
public record Profile(int Id, string Name);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Detailed store member information including verification and team status flags.
|
/// Detailed store member information including verification and team status flags.
|
||||||
@@ -32,22 +13,65 @@ namespace FsTool.Tasks
|
|||||||
/// <param name="Verified">The verification state of the member.</param>
|
/// <param name="Verified">The verification state of the member.</param>
|
||||||
public record Member(int Id, string Name, TeamActiveStatus Team_Active, VerifiedStatus Verified);
|
public record Member(int Id, string Name, TeamActiveStatus Team_Active, VerifiedStatus Verified);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Record Pickup
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a pickup date and the occupied slots for that date.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Date">The pickup date string.</param>
|
||||||
|
/// <param name="OccupiedSlots">The slots already assigned for the date.</param>
|
||||||
|
public record Pickup(string Date, List<Slot> OccupiedSlots);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Record Profile
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal profile information for a store member.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">The Foodsharing profile identifier.</param>
|
||||||
|
/// <param name="Name">The profile display name.</param>
|
||||||
|
public record Profile(int Id, string Name);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Record Slot
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes a booked slot and the profile occupying it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="IsConfirmed">Indicates whether the slot is confirmed.</param>
|
||||||
|
/// <param name="Profile">The profile assigned to the slot.</param>
|
||||||
|
public record Slot(bool IsConfirmed, Profile Profile);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Enum TeamActiveStatus
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether a team member is active or only available as a jumper.
|
/// Indicates whether a team member is active or only available as a jumper.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum TeamActiveStatus
|
public enum TeamActiveStatus
|
||||||
{
|
{
|
||||||
Jumper = 2,
|
Jumper = 2,
|
||||||
Active = 1,
|
Active = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Enum VerifiedStatus
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies whether a member has completed verification.
|
/// Specifies whether a member has completed verification.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum VerifiedStatus
|
public enum VerifiedStatus
|
||||||
{
|
{
|
||||||
Unverified = 0,
|
Unverified = 0,
|
||||||
Verified = 1,
|
Verified = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,48 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using FsTool;
|
using FsToolbox.Cli.Helpers;
|
||||||
using FsTool.Helpers;
|
|
||||||
|
|
||||||
namespace FsTool.Tasks
|
namespace FsToolbox.Cli.Tasks
|
||||||
{
|
{
|
||||||
public static partial class StoreTasks
|
public static partial class StoreTasks
|
||||||
{
|
{
|
||||||
|
#region Public Method GetPickupsAsync
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves all pickups for the specified store with authentication handled automatically.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
||||||
|
/// <param name="storeId">The store identifier to query.</param>
|
||||||
|
/// <returns>A list of pickups, or an empty list when no data is available.</returns>
|
||||||
|
public static async Task<List<Pickup>> GetPickupsAsync(HttpClient httpClient, int storeId)
|
||||||
|
{
|
||||||
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
|
|
||||||
|
var uri = string.Format(Endpoints.StorePickups, storeId);
|
||||||
|
var response = await httpClient.GetAsync(uri);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// handle unsuccessful response
|
||||||
|
if (!response.IsSuccessStatusCode) await Console.Error.WriteLineAsync($"Pickup retrieval failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
|
||||||
|
|
||||||
|
var root = JsonNode.Parse(responseBody);
|
||||||
|
if (root == null) return [];
|
||||||
|
|
||||||
|
// Deserialize JsonNode to List<Pickup>
|
||||||
|
var opts = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
PropertyNamingPolicy = null // <── WICHTIG
|
||||||
|
};
|
||||||
|
|
||||||
|
return root["pickups"].Deserialize<List<Pickup>>(opts) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Method GetStoreMembersAsync
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the members of the specified store, requiring authentication before the request.
|
/// Retrieves the members of the specified store, requiring authentication before the request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -36,45 +71,15 @@ namespace FsTool.Tasks
|
|||||||
var opts = new JsonSerializerOptions
|
var opts = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
PropertyNamingPolicy = null // <── WICHTIG
|
PropertyNamingPolicy = null // <── WICHTIG
|
||||||
};
|
};
|
||||||
|
|
||||||
return root.Deserialize<List<Member>>(opts) ?? [];
|
return root.Deserialize<List<Member>>(opts) ?? [];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
#endregion
|
||||||
/// Retrieves all pickups for the specified store with authentication handled automatically.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
|
||||||
/// <param name="storeId">The store identifier to query.</param>
|
|
||||||
/// <returns>A list of pickups, or an empty list when no data is available.</returns>
|
|
||||||
public static async Task<List<Pickup>> GetPickupsAsync(HttpClient httpClient, int storeId)
|
|
||||||
{
|
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
|
||||||
|
|
||||||
var uri = string.Format(Endpoints.StorePickups, storeId);
|
#region Public Method PatchPickupAsync
|
||||||
var response = await httpClient.GetAsync(uri);
|
|
||||||
var responseBody = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
// handle unsuccessful response
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
await Console.Error.WriteLineAsync($"Pickup retrieval failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var root = JsonNode.Parse(responseBody);
|
|
||||||
if (root == null) return [];
|
|
||||||
|
|
||||||
// Deserialize JsonNode to List<Pickup>
|
|
||||||
var opts = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
PropertyNamingPolicy = null // <── WICHTIG
|
|
||||||
};
|
|
||||||
|
|
||||||
return root["pickups"].Deserialize<List<Pickup>>(opts) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks a pickup slot as confirmed for the specified store, date, and user.
|
/// Marks a pickup slot as confirmed for the specified store, date, and user.
|
||||||
@@ -102,5 +107,7 @@ namespace FsTool.Tasks
|
|||||||
else
|
else
|
||||||
Console.WriteLine($"Pickup patch succeeded {fsId} on {pickupDate}");
|
Console.WriteLine($"Pickup patch succeeded {fsId} on {pickupDate}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using FsTool;
|
using FsToolbox.Cli.Helpers;
|
||||||
using FsTool.Helpers;
|
|
||||||
|
|
||||||
namespace FsTool.Tasks
|
namespace FsToolbox.Cli.Tasks
|
||||||
{
|
{
|
||||||
public static class UserTasks
|
public static class UserTasks
|
||||||
{
|
{
|
||||||
@@ -77,7 +76,7 @@ namespace FsTool.Tasks
|
|||||||
var expiryEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("expires="));
|
var expiryEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("expires="));
|
||||||
var expireString = expiryEntry?.Split('=')[1];
|
var expireString = expiryEntry?.Split('=')[1];
|
||||||
if (DateTime.TryParse(expireString, out var expiration))
|
if (DateTime.TryParse(expireString, out var expiration))
|
||||||
await AuthHelper.StoreCsrfTokenAsync(csrfToken, expiration);
|
await FsToolbox.Cli.Helpers.AuthHelper.StoreCsrfTokenAsync(csrfToken, expiration);
|
||||||
}
|
}
|
||||||
|
|
||||||
return csrfToken;
|
return csrfToken;
|
||||||
@@ -90,7 +89,7 @@ namespace FsTool.Tasks
|
|||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
public static async Task GetCurrentUserAsync(HttpClient httpClient)
|
public static async Task GetCurrentUserAsync(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
await FsToolbox.Cli.Helpers.AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
|
|
||||||
var response = await httpClient.GetAsync(Endpoints.UserCurrent);
|
var response = await httpClient.GetAsync(Endpoints.UserCurrent);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user