fix(auth): send X-Api-Key header — Espo-Authorization alone was wrong for API users

EspoCRM uses TWO different auth schemes:
  * X-Api-Key: <apiKey>            — for API-type users (no password)
  * Espo-Authorization: base64(u:p) — for regular users with passwords

The PRD-derived constructor only set Espo-Authorization with a fake
base64(username:apiKey) which the server rejects with 401 for API users.

Fix:
- Always send X-Api-Key when an apiKey is provided.
- Also send Espo-Authorization Basic-style header when a username is
  supplied, so regular-user credentials still work without code changes.
- Username is now optional (constructor + Settings dialog). API key is
  sufficient on its own; Settings credential store falls back to
  "(api-key)" as the recorded username for logging when blank.

Tests updated:
- FileMailAsync_AddsBothApiKeyAndBasicAuthHeaders — asserts X-Api-Key
  is the primary header and Espo-Authorization remains the fallback.
- Constructor_RejectsBlankCredentials — empty username now allowed,
  empty apiKey / baseUrl still throw.

All 38 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PointStar
2026-05-11 16:22:23 +03:00
parent 661e0cb598
commit 891b9d01ae
3 changed files with 31 additions and 14 deletions
@@ -118,9 +118,9 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
}
var apiKey = ReadApiKey();
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(apiKey))
if (string.IsNullOrWhiteSpace(apiKey))
{
Fail("יש להזין שם משתמש ומפתח API");
Fail("יש להזין מפתח API");
return;
}
@@ -128,7 +128,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
try
{
http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
var probe = new EspoCrmClient(http, baseUrl.ToString(), Username, apiKey, _logger);
var probe = new EspoCrmClient(http, baseUrl.ToString(), Username ?? string.Empty, apiKey, _logger);
var user = await probe.TestConnectionAsync().ConfigureAwait(true);
Succeed($"התחברות תקינה — משתמש: {user.UserName ?? user.Id}");
}
@@ -221,9 +221,9 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
return;
}
var apiKey = ReadApiKey();
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(apiKey))
if (string.IsNullOrWhiteSpace(apiKey))
{
Fail("יש להזין שם משתמש ומפתח API");
Fail("יש להזין מפתח API");
return;
}
@@ -240,7 +240,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
_settingsManager.Save(_settings);
_credentialStore.Save(Username.Trim(), apiKey);
_credentialStore.Save(string.IsNullOrWhiteSpace(Username) ? "(api-key)" : Username.Trim(), apiKey);
_logger.Information("Settings saved (user={User}, url={Url}, watched={WatchedCount})",
Username, _settings.EspoCrmUrl, _settings.WatchedFolders.Count);