Modularize project structure by splitting responsibilities into distinct files and namespaces. Add helper methods for authentication and JSON operations.
This commit is contained in:
125
AuthHelper.cs
125
AuthHelper.cs
@@ -1,125 +0,0 @@
|
|||||||
using System.Dynamic;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace FsTool;
|
|
||||||
|
|
||||||
public static class AuthHelper
|
|
||||||
{
|
|
||||||
private static async Task<string?> LoadCsrfTokenAsync()
|
|
||||||
{
|
|
||||||
if (!File.Exists("csrf_token.json")) return null;
|
|
||||||
|
|
||||||
var json = await File.ReadAllTextAsync("csrf_token.json");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task StoreCsrfTokenAsync(string? csrfToken, DateTime expiration)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(csrfToken)) return;
|
|
||||||
|
|
||||||
var tokenInfo = new
|
|
||||||
{
|
|
||||||
token = csrfToken,
|
|
||||||
expiresAt = expiration
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(tokenInfo, new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync("csrf_token.json", json);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task EnsureAuthenticationAsync(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
var csrfToken = await LoadCsrfTokenAsync();
|
|
||||||
csrfToken = null;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(csrfToken))
|
|
||||||
csrfToken = await CallLoginEndpointAsync(httpClient);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(csrfToken))
|
|
||||||
{
|
|
||||||
csrfToken = csrfToken.ReplaceLineEndings(string.Empty);
|
|
||||||
|
|
||||||
httpClient.DefaultRequestHeaders.Remove("X-CSRF-Token");
|
|
||||||
httpClient.DefaultRequestHeaders.Add("X-CSRF-Token", csrfToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string?> CallLoginEndpointAsync(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
string? csrfToken = null;
|
|
||||||
|
|
||||||
// prompt for 2FA code
|
|
||||||
Console.Write("Enter 2FA code: ");
|
|
||||||
var authCode = Console.ReadLine()?.Trim();
|
|
||||||
|
|
||||||
// validate input
|
|
||||||
if (string.IsNullOrWhiteSpace(authCode))
|
|
||||||
{
|
|
||||||
Console.WriteLine("A valid 2FA code is required.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = new
|
|
||||||
{
|
|
||||||
email = "foodsharing@beging.de",
|
|
||||||
password = "z+hc@Ox9Zu4~MXzkB:Z@O.-S1AvsT&mc!oyQA?NK1jckl1Dzi2^-)+.H.AJKBLoi",
|
|
||||||
code = authCode,
|
|
||||||
remember_me = true
|
|
||||||
};
|
|
||||||
var response = await httpClient.PostAsJsonAsync(Endpoints.Login, payload);
|
|
||||||
|
|
||||||
// handle unsuccessful response
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var responseBody = await response.Content.ReadAsStringAsync();
|
|
||||||
await Console.Error.WriteLineAsync($"Login failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read headers as dictionary
|
|
||||||
var headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value));
|
|
||||||
if(headers.TryGetValue("Set-Cookie", out var setCookieHeader))
|
|
||||||
{
|
|
||||||
// Split cookies by comma and semicolon
|
|
||||||
var cookies = setCookieHeader.Split(new[] { ';' });
|
|
||||||
var csrfTokenEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("FS_CSRF_TOKEN="));
|
|
||||||
csrfToken = csrfTokenEntry?.Split('=')[1];
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(csrfToken)) return null;
|
|
||||||
|
|
||||||
var expiryEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("expires="));
|
|
||||||
var expireString = expiryEntry?.Split('=')[1];
|
|
||||||
if (DateTime.TryParse(expireString, out var expiration))
|
|
||||||
await StoreCsrfTokenAsync(csrfToken, expiration);
|
|
||||||
}
|
|
||||||
|
|
||||||
return csrfToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
CustomTasks.Regions.cs
Normal file
12
CustomTasks.Regions.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace FsTool
|
||||||
|
{
|
||||||
|
public partial class CustomTasks
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregates store and member information for ALDI membership analysis.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Store">The store participating in the membership relation.</param>
|
||||||
|
/// <param name="Member">The member associated with the store.</param>
|
||||||
|
private record AldiMember(FsTool.Tasks.RegionTasks.Store Store, FsTool.Tasks.StoreTasks.Member Member);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using FsTool.Helpers;
|
||||||
|
using FsTool.Tasks;
|
||||||
|
|
||||||
namespace FsTool
|
namespace FsTool
|
||||||
{
|
{
|
||||||
public class CustomTasks
|
public partial class CustomTasks
|
||||||
{
|
{
|
||||||
#region Public Method GetUnconfirmedPickupsLindenbergAsync
|
#region Public Method GetUnconfirmedPickupsLindenbergAsync
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Confirms all unconfirmed pickup slots for the configured Lindenberg store after user confirmation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to perform the requests.</param>
|
||||||
public static async Task ConfirmUnconfirmedPickupsLindenbergAsync(HttpClient httpClient)
|
public static async Task ConfirmUnconfirmedPickupsLindenbergAsync(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
@@ -52,8 +58,10 @@ namespace FsTool
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private record AldiMember(RegionTasks.Store Store, StoreTasks.Member Member);
|
/// <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)
|
public static async Task CheckAldiMembershipsAsync(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public static class Endpoints
|
public static class Endpoints
|
||||||
{
|
{
|
||||||
public const string Login = "https://beta.foodsharing.de/api/user/login";
|
public const string UserLogin = "https://beta.foodsharing.de/api/user/login";
|
||||||
|
|
||||||
public const string UserCurrent = "https://beta.foodsharing.de/api/user/current";
|
public const string UserCurrent = "https://beta.foodsharing.de/api/user/current";
|
||||||
|
|
||||||
|
|||||||
80
Helper/AuthHelper.cs
Normal file
80
Helper/AuthHelper.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using FsTool.Tasks;
|
||||||
|
|
||||||
|
namespace FsTool.Helpers;
|
||||||
|
|
||||||
|
public static class AuthHelper
|
||||||
|
{
|
||||||
|
private static async Task<string?> LoadCsrfTokenAsync()
|
||||||
|
{
|
||||||
|
if (!File.Exists("csrf_token.json")) return null;
|
||||||
|
|
||||||
|
var json = await File.ReadAllTextAsync("csrf_token.json");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task StoreCsrfTokenAsync(string? csrfToken, DateTime expiration)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(csrfToken)) return;
|
||||||
|
|
||||||
|
var tokenInfo = new
|
||||||
|
{
|
||||||
|
token = csrfToken,
|
||||||
|
expiresAt = expiration
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(tokenInfo, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync("csrf_token.json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task EnsureAuthenticationAsync(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
var csrfToken = await LoadCsrfTokenAsync();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
namespace FsTool
|
namespace FsTool.Helpers
|
||||||
{
|
{
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
27
Tasks/RegionTasks.Regions.cs
Normal file
27
Tasks/RegionTasks.Regions.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace FsTool.Tasks
|
||||||
|
{
|
||||||
|
public partial class RegionTasks
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Basic region store information including cooperation status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">The store identifier.</param>
|
||||||
|
/// <param name="Name">The store name.</param>
|
||||||
|
/// <param name="CooperationStatus">The cooperation status of the store.</param>
|
||||||
|
public record Store(int Id, string Name, CooperationStatus CooperationStatus);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the cooperation state between the store and the organization.
|
||||||
|
/// </summary>
|
||||||
|
public enum CooperationStatus
|
||||||
|
{
|
||||||
|
NoStatus = 0,
|
||||||
|
NoContact = 1,
|
||||||
|
Negotiating = 2,
|
||||||
|
DoNotWant = 4,
|
||||||
|
Cooperating = 5,
|
||||||
|
DonatingToTafel = 6,
|
||||||
|
NoExisting = 7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
using FsTool;
|
||||||
|
using FsTool.Helpers;
|
||||||
|
|
||||||
namespace FsTool
|
namespace FsTool.Tasks
|
||||||
{
|
{
|
||||||
public class RegionTasks
|
public partial class RegionTasks
|
||||||
{
|
{
|
||||||
public record Store(int Id, string Name, CooperationStatus CooperationStatus);
|
/// <summary>
|
||||||
|
/// Retrieves all stores within the specified region, ensuring the request is authenticated.
|
||||||
public enum CooperationStatus
|
/// </summary>
|
||||||
{
|
/// <param name="httpClient">The HTTP client used to perform the request.</param>
|
||||||
NoStatus = 0,
|
/// <param name="regionId">The region identifier to query.</param>
|
||||||
NoContact = 1,
|
/// <returns>A list of stores, or an empty list when the call fails or returns no data.</returns>
|
||||||
Negotiating = 2,
|
|
||||||
DoNotWant = 4,
|
|
||||||
Cooperating = 5,
|
|
||||||
DonatingToTafel = 6,
|
|
||||||
NoExisting = 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<List<Store>> GetStoresInRegionAsync(HttpClient httpClient, int regionId)
|
public static async Task<List<Store>> GetStoresInRegionAsync(HttpClient httpClient, int regionId)
|
||||||
{
|
{
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
53
Tasks/StoreTasks.Regions.cs
Normal file
53
Tasks/StoreTasks.Regions.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace FsTool.Tasks
|
||||||
|
{
|
||||||
|
public static partial class StoreTasks
|
||||||
|
{
|
||||||
|
/// <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);
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// Detailed store member information including verification and team status flags.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">The member identifier.</param>
|
||||||
|
/// <param name="Name">The member name.</param>
|
||||||
|
/// <param name="Team_Active">The member's active status within the team.</param>
|
||||||
|
/// <param name="Verified">The verification state of the member.</param>
|
||||||
|
public record Member(int Id, string Name, TeamActiveStatus Team_Active, VerifiedStatus Verified);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether a team member is active or only available as a jumper.
|
||||||
|
/// </summary>
|
||||||
|
public enum TeamActiveStatus
|
||||||
|
{
|
||||||
|
Jumper = 2,
|
||||||
|
Active = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies whether a member has completed verification.
|
||||||
|
/// </summary>
|
||||||
|
public enum VerifiedStatus
|
||||||
|
{
|
||||||
|
Unverified = 0,
|
||||||
|
Verified = 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,19 @@
|
|||||||
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 System.Text.Json.Serialization;
|
using FsTool;
|
||||||
|
using FsTool.Helpers;
|
||||||
|
|
||||||
namespace FsTool
|
namespace FsTool.Tasks
|
||||||
{
|
{
|
||||||
public static class StoreTasks
|
public static partial class StoreTasks
|
||||||
{
|
{
|
||||||
public record Pickup(string Date, List<Slot> OccupiedSlots);
|
/// <summary>
|
||||||
public record Slot(bool IsConfirmed, Profile Profile);
|
/// Retrieves the members of the specified store, requiring authentication before the request.
|
||||||
public record Profile(int Id, string Name);
|
/// </summary>
|
||||||
public record Member(int Id, string Name, TeamActiveStatus Team_Active, VerifiedStatus Verified);
|
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
||||||
|
/// <param name="storeId">The store identifier to query.</param>
|
||||||
public enum TeamActiveStatus
|
/// <returns>A list of store members, or an empty list when the call fails.</returns>
|
||||||
{
|
|
||||||
Jumper = 2,
|
|
||||||
Active = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum VerifiedStatus
|
|
||||||
{
|
|
||||||
Unverified = 0,
|
|
||||||
Verified = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<List<Member>> GetStoreMembersAsync(HttpClient httpClient, int storeId)
|
public static async Task<List<Member>> GetStoreMembersAsync(HttpClient httpClient, int storeId)
|
||||||
{
|
{
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
@@ -53,6 +43,12 @@ namespace FsTool
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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)
|
public static async Task<List<Pickup>> GetPickupsAsync(HttpClient httpClient, int storeId)
|
||||||
{
|
{
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
@@ -80,6 +76,14 @@ namespace FsTool
|
|||||||
return root["pickups"].Deserialize<List<Pickup>>(opts) ?? [];
|
return root["pickups"].Deserialize<List<Pickup>>(opts) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a pickup slot as confirmed for the specified store, date, and user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
||||||
|
/// <param name="storeId">The store identifier containing the pickup.</param>
|
||||||
|
/// <param name="pickupDate">The date of the pickup to patch.</param>
|
||||||
|
/// <param name="fsId">The Foodsharing user identifier associated with the slot.</param>
|
||||||
|
/// <returns>A task representing the asynchronous operation.</returns>
|
||||||
public static async Task PatchPickupAsync(HttpClient httpClient, int storeId, string pickupDate, int fsId)
|
public static async Task PatchPickupAsync(HttpClient httpClient, int storeId, string pickupDate, int fsId)
|
||||||
{
|
{
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
92
Tasks/UserTasks.cs
Normal file
92
Tasks/UserTasks.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using FsTool;
|
||||||
|
using FsTool.Helpers;
|
||||||
|
|
||||||
|
namespace FsTool.Tasks
|
||||||
|
{
|
||||||
|
public static class UserTasks
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a login request using two-factor authentication and returns the CSRF token from the response.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
||||||
|
/// <returns>The CSRF token when login succeeds; otherwise, <c>null</c>.</returns>
|
||||||
|
public static async Task<string?> CallLoginEndpointAsync(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
string? csrfToken = null;
|
||||||
|
|
||||||
|
// prompt for 2FA code
|
||||||
|
Console.Write("Enter 2FA code: ");
|
||||||
|
var authCode = Console.ReadLine()?.Trim();
|
||||||
|
|
||||||
|
// validate input
|
||||||
|
if (string.IsNullOrWhiteSpace(authCode))
|
||||||
|
{
|
||||||
|
Console.WriteLine("A valid 2FA code is required.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
email = "foodsharing@beging.de",
|
||||||
|
password = "z+hc@Ox9Zu4~MXzkB:Z@O.-S1AvsT&mc!oyQA?NK1jckl1Dzi2^-)+.H.AJKBLoi",
|
||||||
|
code = authCode,
|
||||||
|
remember_me = true
|
||||||
|
};
|
||||||
|
var response = await httpClient.PostAsJsonAsync(Endpoints.UserLogin, payload);
|
||||||
|
|
||||||
|
// handle unsuccessful response
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
await Console.Error.WriteLineAsync($"Login failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read headers as dictionary
|
||||||
|
var headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value));
|
||||||
|
if (headers.TryGetValue("Set-Cookie", out var setCookieHeader))
|
||||||
|
{
|
||||||
|
// Split cookies by comma and semicolon
|
||||||
|
var cookies = setCookieHeader.Split([";"], StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var csrfTokenEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("FS_CSRF_TOKEN="));
|
||||||
|
csrfToken = csrfTokenEntry?.Split('=')[1];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(csrfToken)) return null;
|
||||||
|
|
||||||
|
var expiryEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("expires="));
|
||||||
|
var expireString = expiryEntry?.Split('=')[1];
|
||||||
|
if (DateTime.TryParse(expireString, out var expiration))
|
||||||
|
await AuthHelper.StoreCsrfTokenAsync(csrfToken, expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves information about the current authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
public static async Task GetCurrentUserAsync(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync(Endpoints.UserCurrent);
|
||||||
|
|
||||||
|
// handle unsuccessful response
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
await Console.Error.WriteLineAsync($"Get current user failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Console.WriteLine("Current User Info:");
|
||||||
|
Console.WriteLine(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
UserTasks.cs
24
UserTasks.cs
@@ -1,24 +0,0 @@
|
|||||||
namespace FsTool
|
|
||||||
{
|
|
||||||
public static class UserTasks
|
|
||||||
{
|
|
||||||
public static async Task GetCurrentUserAsync(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
|
||||||
|
|
||||||
var response = await httpClient.GetAsync(Endpoints.UserCurrent);
|
|
||||||
|
|
||||||
// handle unsuccessful response
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var responseBody = await response.Content.ReadAsStringAsync();
|
|
||||||
await Console.Error.WriteLineAsync($"Get current user failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
|
||||||
Console.WriteLine("Current User Info:");
|
|
||||||
Console.WriteLine(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user