LoginFixes

This commit is contained in:
2025-12-18 15:35:39 +01:00
parent c11b361655
commit 5fd97deada
4 changed files with 177 additions and 173 deletions

View File

@@ -13,62 +13,62 @@ internal sealed class ApiClient : IApiClient {
private readonly ApiOptions _options;
private readonly IAppSettings _settings;
public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) {
_http = http;
_options = options;
_settings = settings;
public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) {
_http = http;
_options = options;
_settings = settings;
// Timeout nur einmalig beim Erstellen setzen spätere Änderungen an HttpClient.Timeout
// nach der ersten Verwendung führen zu InvalidOperationException.
if (_http.Timeout != options.Timeout)
_http.Timeout = options.Timeout;
// Standardmäßig JSON akzeptieren; doppelte Einträge vermeiden
if (!_http.DefaultRequestHeaders.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true))
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Timeout nur einmalig beim Erstellen setzen spätere Änderungen an HttpClient.Timeout
// nach der ersten Verwendung führen zu InvalidOperationException.
if (_http.Timeout != options.Timeout)
_http.Timeout = options.Timeout;
// Standardmäßig JSON akzeptieren; doppelte Einträge vermeiden
if (!_http.DefaultRequestHeaders.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true))
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// KEINE globalen Header/Properties mehr dynamisch setzen. Authorization wird pro Request gesetzt.
// KEINE globalen Header/Properties mehr dynamisch setzen. Authorization wird pro Request gesetzt.
_json = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
_json = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
// Globale Converter: erlauben numerische Felder auch als Strings (z.B. user.id)
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleInt32Converter());
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleNullableInt32Converter());
// WICHTIG: HttpClient.BaseAddress NICHT dynamisch setzen oder ändern das verursacht Exceptions,
// sobald bereits Requests gestartet wurden. Wir bauen stattdessen absolute URIs pro Request.
}
// WICHTIG: HttpClient.BaseAddress NICHT dynamisch setzen oder ändern das verursacht Exceptions,
// sobald bereits Requests gestartet wurden. Wir bauen stattdessen absolute URIs pro Request.
}
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> SendAsync<T>(HttpMethod.Get, path, null, query, ct);
public async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? query = null, CancellationToken ct = default) {
// Absolute URI aus aktuellem SettingsBaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen.
var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query);
using var req = new HttpRequestMessage(method, uri);
// Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist)
// Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten.
// AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen.
var currentToken = _settings.ApiKey;
if (!string.IsNullOrWhiteSpace(currentToken)) {
// Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden
req.Headers.Remove("Authorization");
public async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? query = null, CancellationToken ct = default) {
// Absolute URI aus aktuellem SettingsBaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen.
var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query);
using var req = new HttpRequestMessage(method, uri);
// Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist)
// Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten.
// AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen.
var currentToken = _settings.ApiKey;
if (!string.IsNullOrWhiteSpace(currentToken)) {
// Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden
req.Headers.Remove("Authorization");
req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {currentToken}");
}
if (body is HttpContent httpContent) {
req.Content = httpContent;
} else if (body is not null) {
req.Content = JsonContent.Create(body, options: _json);
}
if (body is HttpContent httpContent) {
req.Content = httpContent;
} else if (body is not null) {
req.Content = JsonContent.Create(body, options: _json);
}
// Sicherstellen, dass Accept: application/json auch auf Request-Ebene vorhanden ist (z. B. für LoginWithToken GET)
if (!req.Headers.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) {
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
// Sicherstellen, dass Accept: application/json auch auf Request-Ebene vorhanden ist (z. B. für LoginWithToken GET)
if (!req.Headers.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) {
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
@@ -109,7 +109,7 @@ internal sealed class ApiClient : IApiClient {
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> SendAsync<object>(HttpMethod.Delete, path, null, query, ct);
// Entfernt: EnsureBaseAddress wir ändern BaseAddress nicht mehr dynamisch.
// Entfernt: EnsureBaseAddress wir ändern BaseAddress nicht mehr dynamisch.
private static string TryGetMessage(string text) {
try {
@@ -122,29 +122,29 @@ internal sealed class ApiClient : IApiClient {
return text;
}
private static Uri BuildAbsoluteUri(string baseUrl, string path, IDictionary<string, string?>? query) {
if (string.IsNullOrWhiteSpace(baseUrl))
throw new InvalidOperationException(
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl').");
private static Uri BuildAbsoluteUri(string baseUrl, string path, IDictionary<string, string?>? query) {
if (string.IsNullOrWhiteSpace(baseUrl))
throw new InvalidOperationException(
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl').");
// Basis muss absolut sein (z. B. https://host/appapi/)
var baseUri = new Uri(baseUrl, UriKind.Absolute);
// Basis muss absolut sein (z. B. https://host/appapi/)
var baseUri = new Uri(baseUrl, UriKind.Absolute);
// Pfad relativ zur Basis aufbauen
string relativePath = path ?? string.Empty;
if (query is not null && query.Count > 0) {
var sb = new StringBuilder(relativePath);
sb.Append(relativePath.Contains('?') ? '&' : '?');
sb.Append(string.Join('&', query
.Where(kv => kv.Value is not null)
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
relativePath = sb.ToString();
}
// Pfad relativ zur Basis aufbauen
string relativePath = path ?? string.Empty;
if (query is not null && query.Count > 0) {
var sb = new StringBuilder(relativePath);
sb.Append(relativePath.Contains('?') ? '&' : '?');
sb.Append(string.Join('&', query
.Where(kv => kv.Value is not null)
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
relativePath = sb.ToString();
}
// Wenn path bereits absolut ist, direkt verwenden
if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath))
return absoluteFromPath;
// Wenn path bereits absolut ist, direkt verwenden
if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath))
return absoluteFromPath;
return new Uri(baseUri, relativePath);
}
return new Uri(baseUri, relativePath);
}
}