feat: complete phase six backoffice operations

This commit is contained in:
2026-07-28 14:31:43 +08:00
parent 747ff59d76
commit 99e4e43122
31 changed files with 23504 additions and 26 deletions

View File

@@ -34,6 +34,24 @@ internal static class AuthenticationTestClientExtensions
return await client.CompleteTenantAuthenticationAsync(response, tenantId, identifier);
}
public static async Task<TestAuthenticationTokens> LoginAsPlatformAsync(
this HttpClient client,
string identifier,
string password = PasswordTestUserExtensions.TestPassword)
{
client.DefaultRequestHeaders.Remove("x-tenant-code");
var response = await client.PostAsJsonAsync(
"/api/auth/login/password",
new PasswordLoginDto
{
Realm = AuthRealm.Platform,
Identifier = identifier,
Password = password
});
return await client.CompletePlatformAuthenticationAsync(response, identifier);
}
public static async Task<TestAuthenticationTokens> CompleteTenantAuthenticationAsync(
this HttpClient client,
HttpResponseMessage response,
@@ -96,6 +114,67 @@ internal static class AuthenticationTestClientExtensions
throw new InvalidOperationException($"Unsupported test authentication status '{status}'.");
}
private static async Task<TestAuthenticationTokens> CompletePlatformAuthenticationAsync(
this HttpClient client,
HttpResponseMessage response,
string authenticatorCacheKey)
{
client.DefaultRequestHeaders.Remove("x-tenant-code");
using var authentication = await ReadSuccessfulJsonAsync(response);
var root = authentication.RootElement;
var status = root.GetProperty("status").GetString();
if (string.Equals(status, "authenticated", StringComparison.OrdinalIgnoreCase))
{
return ReadTokens(root.GetProperty("user").GetProperty("tokens"));
}
var challengeToken = root.GetProperty("challengeToken").GetString()
?? throw new InvalidOperationException("Authentication challenge did not contain a challenge token.");
var keyId = $"platform:{authenticatorCacheKey}";
if (string.Equals(status, "mfa_enrollment_required", StringComparison.OrdinalIgnoreCase))
{
var setupResponse = await client.PostAsJsonAsync(
"/api/auth/mfa/totp/setup",
new MfaChallengeDto { ChallengeToken = challengeToken });
using var setup = await ReadSuccessfulJsonAsync(setupResponse);
var sharedKey = setup.RootElement.GetProperty("sharedKey").GetString()
?? throw new InvalidOperationException("MFA setup did not return a shared key.");
AuthenticatorKeys[keyId] = sharedKey;
var confirmResponse = await client.PostAsJsonAsync(
"/api/auth/mfa/totp/confirm",
new MfaChallengeDto
{
ChallengeToken = challengeToken,
Code = GenerateTotp(sharedKey)
});
using var confirmation = await ReadSuccessfulJsonAsync(confirmResponse);
return ReadTokens(
confirmation.RootElement
.GetProperty("authentication")
.GetProperty("user")
.GetProperty("tokens"));
}
if (string.Equals(status, "mfa_required", StringComparison.OrdinalIgnoreCase) &&
AuthenticatorKeys.TryGetValue(keyId, out var existingKey))
{
var verifyResponse = await client.PostAsJsonAsync(
"/api/auth/mfa/totp/verify",
new MfaChallengeDto
{
ChallengeToken = challengeToken,
Code = GenerateTotp(existingKey)
});
using var verification = await ReadSuccessfulJsonAsync(verifyResponse);
return ReadTokens(verification.RootElement.GetProperty("user").GetProperty("tokens"));
}
throw new InvalidOperationException($"Unsupported test authentication status '{status}'.");
}
public static void UseAccessToken(this HttpClient client, TestAuthenticationTokens tokens)
{
client.DefaultRequestHeaders.Authorization = new("Bearer", tokens.AccessToken);