Ichen Chhoeng ff0e97e36b blah
2025-11-17 11:25:33 +07:00

261 lines
9.3 KiB
C#

using System.Text.Json;
using Ory.Kratos.Client.Api;
using khmereid_backend.Dtos;
using Ory.Kratos.Client.Model;
using Ory.Kratos.Client.Client;
namespace khmereid_backend.Integrations;
public class KratosApi : IAuthnApi
{
private readonly FrontendApi _frontendApi;
private readonly IdentityApi _identityApi;
private readonly ILogger<KratosApi> _logger;
public KratosApi(IConfiguration config, ILogger<KratosApi> logger)
{
var publicCfg = new Configuration { BasePath = config["Ory:Kratos:PublicUrl"]! };
var adminCfg = new Configuration { BasePath = config["Ory:Kratos:AdminUrl"]! };
_frontendApi = new FrontendApi(publicCfg);
_identityApi = new IdentityApi(adminCfg);
_logger = logger;
}
// --------------------------- Registration Flows ---------------------------
public async Task<FlowDto> CreateOtpRegistrationFlowAsync(string phone)
{
var flow = await _frontendApi.CreateNativeRegistrationFlowAsync(returnSessionTokenExchangeCode: true);
_logger.LogInformation("Started registration flow: {FlowId}", flow.Id);
phone = phone.Trim();
var method = new KratosUpdateRegistrationFlowWithCodeMethod(method: "code", traits: new { phone });
var body = new KratosUpdateRegistrationFlowBody(method);
try
{
var updatedFlow = await _frontendApi.UpdateRegistrationFlowAsync(flow.Id, body);
return new FlowDto{};
}
catch (ApiException ex)
{
return HandleFlowException(ex);
}
}
public async Task<SignupResultDto> CompleteOtpRegistrationFlowAsync(string otp, string phone, string flowId)
{
phone = phone.Trim();
var method = new KratosUpdateRegistrationFlowWithCodeMethod(code: otp, method: "code", traits: new { phone });
var body = new KratosUpdateRegistrationFlowBody(method);
try
{
var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body);
_logger.LogInformation("Completed OTP registration flow for flow {FlowId}", flowId);
return new SignupResultDto
{
IdentityId = Guid.Parse(result.Identity.Id),
AccessToken = result.SessionToken,
ExpiredAt = result.Session?.ExpiresAt
};
}
catch (ApiException ex)
{
return HandleRegistrationException(ex);
}
}
// --------------------------- Login Flows ---------------------------
public async Task<FlowDto> CreateOtpLoginFlowAsync(string phone)
{
var flow = await _frontendApi.CreateNativeLoginFlowAsync(returnSessionTokenExchangeCode: true);
_logger.LogInformation("Started login flow: {FlowId}", flow.Id);
phone = phone.Trim();
var method = new KratosUpdateLoginFlowWithCodeMethod(method: "code", csrfToken: "dfdfdfde", identifier: phone);
var body = new KratosUpdateLoginFlowBody(method);
try
{
var updatedFlow = await _frontendApi.UpdateLoginFlowAsync(flow.Id, body);
return new FlowDto{};
}
catch (ApiException ex)
{
return HandleFlowException(ex);
}
}
public async Task<LoginResultDto> CompleteOtpLoginFlowAsync(string flowId, string phone, string otp)
{
phone = "+" + phone.Trim();
var method = new KratosUpdateLoginFlowWithCodeMethod(code: otp, method: "code", csrfToken: "dfdfdfde", identifier: phone);
var body = new KratosUpdateLoginFlowBody(method);
try
{
var result = await _frontendApi.UpdateLoginFlowAsync(flowId, body);
_logger.LogInformation("Completed OTP login flow for flow {FlowId}", flowId);
return new LoginResultDto
{
AccessToken = result.SessionToken,
ExpiredAt = result.Session?.ExpiresAt
};
}
catch (ApiException ex)
{
return HandleLoginException(ex);
}
}
// --------------------------- Session & Logout ---------------------------
public async Task<SessionDto> GetMeAsync(string sessionToken)
{
try
{
var session = await _frontendApi.ToSessionAsync(xSessionToken: sessionToken);
return new SessionDto
{
Id = session.Identity.Id,
ExpiresAt = session.ExpiresAt
};
}
catch (ApiException ex) when (ex.ErrorCode == 401 || ex.ErrorCode == 403)
{
throw new UnauthorizedAccessException("Invalid or expired Kratos session token", ex);
}
catch (ApiException ex)
{
throw new InvalidOperationException($"Failed to fetch session from Kratos: {ex.Message}", ex);
}
}
public async Task<dynamic> Logout(string sessionToken)
{
var body = new KratosPerformNativeLogoutBody(sessionToken: sessionToken);
await _frontendApi.PerformNativeLogoutAsync(body);
_logger.LogInformation("Logged out session with token {SessionToken}", sessionToken);
return new { Message = "Logged out successfully." };
}
// --------------------------- Password Flow ---------------------------
public async Task<dynamic> PasswordRegistrationFlowAsync(string flowId, string phone)
{
var method = new KratosUpdateRegistrationFlowWithPasswordMethod(
password: "add3ae4d8ae8",
traits: new { phone, identifier = phone },
method: "password"
);
var body = new KratosUpdateRegistrationFlowBody(method);
var result = await _frontendApi.UpdateRegistrationFlowAsync(flowId, body);
_logger.LogInformation("Completed password registration flow for {Phone}", phone);
return result;
}
// --------------------------- Private Helpers ---------------------------
private static FlowDto HandleFlowException(ApiException ex)
{
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent?.ToString() ?? "{}");
if (ex.ErrorCode == 400 && res.TryGetProperty("state", out var stateProp) && stateProp.GetString() == "sent_email")
{
return new FlowDto
{
FlowId = res.GetProperty("id").GetString() ?? "",
State = "sent_otp",
ExpiresAt = res.TryGetProperty("expires_at", out var expiresAtProp) ? expiresAtProp.GetDateTime() : (DateTimeOffset?)null
};
}
if (ex.ErrorCode == 400)
return new FlowDto
{
Error = "BadRequest",
Code = ex.ErrorCode.ToString(),
Message = GetKratosErrorMessage(res)
};
throw new InvalidOperationException($"Kratos API returned {ex.ErrorCode}: {ex.Message}", ex);
}
private SignupResultDto HandleRegistrationException(ApiException ex)
{
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent?.ToString() ?? "{}");
if (ex.ErrorCode == 410 && res.TryGetProperty("use_flow_id", out var useFlowProp))
{
_logger.LogWarning("Registration flow expired. Use new flow: {FlowId}", useFlowProp.GetString());
return new SignupResultDto
{
Error = "FlowExpired",
Code = ex.ErrorCode.ToString(),
Message = "Registration flow expired. Please restart registration.",
UseFlowId = useFlowProp.GetString()
};
}
if (ex.ErrorCode == 400)
return new SignupResultDto
{
Code = ex.ErrorCode.ToString(),
Error = "BadRequest",
Message = GetKratosErrorMessage(res)
};
if (ex.ErrorCode == 404)
return new SignupResultDto
{
Code = ex.ErrorCode.ToString(),
Error = "NotFound",
Message = "Registration flow not found."
};
return new SignupResultDto
{
Code = ex.ErrorCode.ToString(),
Error = "UnhandledException",
Message = $"Unhandled Kratos API exception: {ex.Message}"
};
}
private static LoginResultDto HandleLoginException(ApiException ex)
{
var res = JsonSerializer.Deserialize<JsonElement>(ex.ErrorContent?.ToString() ?? "{}");
if (ex.ErrorCode == 404)
return new LoginResultDto
{
Error = "NotFound",
Code = ex.ErrorCode.ToString(),
Message = "Login flow not found."
};
return new LoginResultDto
{
Error = "BadRequest",
Code = ex.ErrorCode.ToString(),
Message = GetKratosErrorMessage(res)
};
}
private static string GetKratosErrorMessage(JsonElement res)
{
if (res.TryGetProperty("error", out var errProp) && errProp.TryGetProperty("reason", out var reasonProp))
return reasonProp.GetString() ?? "Bad request";
if (res.TryGetProperty("ui", out var uiProp) &&
uiProp.TryGetProperty("messages", out var messagesProp) &&
messagesProp.ValueKind == JsonValueKind.Array &&
messagesProp.GetArrayLength() > 0 &&
messagesProp[0].TryGetProperty("text", out var textProp))
return textProp.GetString() ?? "Bad request";
return "Bad request";
}
}