feat(wbs): AEG-VS-01-04 BE Vertical Slice - Endpoints & Handlers (Part 1)
- RegisterIdentity endpoint (POST /api/identities)
- RequestMfaSetup endpoint (PUT /api/identities/{id}/request-mfa)
- SQL repositories w/ optimistic concurrency (revision tracking)
- Application handlers (IEndpointHandler pattern)
- ValidationException + ProblemDetails error handling
- Unit tests: RegisterIdentityHandlerTests (4), RequestMfaSetupHandlerTests (4)
- Domain state machines integrated (IdentityState lifecycle)
- AGENTS.md v16.0: endpoint authority, idempotency, correlation ID ready
DI registration & integration tests deferred to next session.
17 new files, 500+ LOC, 8/8 unit tests ready to run
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
|
||||
namespace KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RequestMfaSetup;
|
||||
|
||||
public interface IRequestMfaSetupSql
|
||||
{
|
||||
Task<(Guid Id, string State, int Revision)> GetIdentityAsync(Guid identityId, CancellationToken ct);
|
||||
Task UpdateIdentityStateAsync(Guid identityId, string newState, int expectedRevision, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class RequestMfaSetupSql : IRequestMfaSetupSql
|
||||
{
|
||||
private readonly Func<Task<NpgsqlConnection>> _connectionFactory;
|
||||
|
||||
public RequestMfaSetupSql(Func<Task<NpgsqlConnection>> connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<(Guid Id, string State, int Revision)> GetIdentityAsync(Guid identityId, CancellationToken ct)
|
||||
{
|
||||
using var conn = await _connectionFactory();
|
||||
const string sql = """
|
||||
SELECT id, state, revision_version
|
||||
FROM identity.identity
|
||||
WHERE id = @identityId
|
||||
""";
|
||||
|
||||
var row = await conn.QuerySingleOrDefaultAsync<dynamic>(sql, new { identityId }, commandTimeout: 5);
|
||||
if (row is null)
|
||||
throw new InvalidOperationException($"Identity {identityId} not found");
|
||||
|
||||
return ((Guid)row.id, (string)row.state, (int)row.revision_version);
|
||||
}
|
||||
|
||||
public async Task UpdateIdentityStateAsync(Guid identityId, string newState, int expectedRevision, CancellationToken ct)
|
||||
{
|
||||
using var conn = await _connectionFactory();
|
||||
const string sql = """
|
||||
UPDATE identity.identity
|
||||
SET state = @newState,
|
||||
revision_version = revision_version + 1,
|
||||
updated_at = NOW(),
|
||||
published_at = NOW()
|
||||
WHERE id = @identityId AND revision_version = @expectedRevision
|
||||
""";
|
||||
|
||||
var rowsAffected = await conn.ExecuteAsync(sql, new
|
||||
{
|
||||
identityId,
|
||||
newState,
|
||||
expectedRevision
|
||||
}, commandTimeout: 5);
|
||||
|
||||
if (rowsAffected == 0)
|
||||
throw new InvalidOperationException("Optimistic concurrency violation: state changed");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user