up
This commit is contained in:
@@ -0,0 +1,136 @@
|
|||||||
|
using StellaOps.Authority.Storage.Postgres.Models;
|
||||||
|
using StellaOps.Infrastructure.Postgres;
|
||||||
|
|
||||||
|
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||||
|
|
||||||
|
public sealed class AuditRepository : RepositoryBase<AuthorityDataSource>, IAuditRepository
|
||||||
|
{
|
||||||
|
public AuditRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||||
|
|
||||||
|
public async Task<long> CreateAsync(string tenantId, AuditEntity audit, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO authority.audit (tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent, correlation_id)
|
||||||
|
VALUES (@tenant_id, @user_id, @action, @resource_type, @resource_id, @old_value::jsonb, @new_value::jsonb, @ip_address, @user_agent, @correlation_id)
|
||||||
|
RETURNING id
|
||||||
|
""";
|
||||||
|
await using var connection = await DataSource.OpenConnectionAsync(tenantId, DataSourceRole.Writer, cancellationToken).ConfigureAwait(false);
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = sql;
|
||||||
|
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||||
|
AddNullableParameter(command, "user_id", audit.UserId);
|
||||||
|
command.Parameters.AddWithValue("action", audit.Action);
|
||||||
|
command.Parameters.AddWithValue("resource_type", audit.ResourceType);
|
||||||
|
AddNullableParameter(command, "resource_id", audit.ResourceId);
|
||||||
|
AddNullableJsonbParameter(command, "old_value", audit.OldValue);
|
||||||
|
AddNullableJsonbParameter(command, "new_value", audit.NewValue);
|
||||||
|
AddNullableParameter(command, "ip_address", audit.IpAddress);
|
||||||
|
AddNullableParameter(command, "user_agent", audit.UserAgent);
|
||||||
|
AddNullableParameter(command, "correlation_id", audit.CorrelationId);
|
||||||
|
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return (long)result!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent, correlation_id, created_at
|
||||||
|
FROM authority.audit
|
||||||
|
WHERE tenant_id = @tenant_id
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT @limit OFFSET @offset
|
||||||
|
""";
|
||||||
|
return await QueryAsync(tenantId, sql, MapAudit, cmd =>
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("limit", limit);
|
||||||
|
cmd.Parameters.AddWithValue("offset", offset);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AuditEntity>> GetByUserIdAsync(string tenantId, Guid userId, int limit = 100, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent, correlation_id, created_at
|
||||||
|
FROM authority.audit
|
||||||
|
WHERE tenant_id = @tenant_id AND user_id = @user_id
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT @limit
|
||||||
|
""";
|
||||||
|
return await QueryAsync(tenantId, sql, MapAudit, cmd =>
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("user_id", userId);
|
||||||
|
cmd.Parameters.AddWithValue("limit", limit);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId, int limit = 100, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var sql = $"""
|
||||||
|
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent, correlation_id, created_at
|
||||||
|
FROM authority.audit
|
||||||
|
WHERE tenant_id = @tenant_id AND resource_type = @resource_type
|
||||||
|
{(resourceId != null ? "AND resource_id = @resource_id" : "")}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT @limit
|
||||||
|
""";
|
||||||
|
return await QueryAsync(tenantId, sql, MapAudit, cmd =>
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("resource_type", resourceType);
|
||||||
|
if (resourceId != null) cmd.Parameters.AddWithValue("resource_id", resourceId);
|
||||||
|
cmd.Parameters.AddWithValue("limit", limit);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent, correlation_id, created_at
|
||||||
|
FROM authority.audit
|
||||||
|
WHERE tenant_id = @tenant_id AND correlation_id = @correlation_id
|
||||||
|
ORDER BY created_at
|
||||||
|
""";
|
||||||
|
return await QueryAsync(tenantId, sql, MapAudit,
|
||||||
|
cmd => { cmd.Parameters.AddWithValue("correlation_id", correlationId); },
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AuditEntity>> GetByActionAsync(string tenantId, string action, int limit = 100, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent, correlation_id, created_at
|
||||||
|
FROM authority.audit
|
||||||
|
WHERE tenant_id = @tenant_id AND action = @action
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT @limit
|
||||||
|
""";
|
||||||
|
return await QueryAsync(tenantId, sql, MapAudit, cmd =>
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("action", action);
|
||||||
|
cmd.Parameters.AddWithValue("limit", limit);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddNullableJsonbParameter(Npgsql.NpgsqlCommand cmd, string name, string? value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
cmd.Parameters.AddWithValue(name, DBNull.Value);
|
||||||
|
else
|
||||||
|
AddJsonbParameter(cmd, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuditEntity MapAudit(System.Data.Common.DbDataReader reader) => new()
|
||||||
|
{
|
||||||
|
Id = reader.GetInt64(0),
|
||||||
|
TenantId = reader.GetString(1),
|
||||||
|
UserId = reader.IsDBNull(2) ? null : reader.GetGuid(2),
|
||||||
|
Action = reader.GetString(3),
|
||||||
|
ResourceType = reader.GetString(4),
|
||||||
|
ResourceId = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||||
|
OldValue = reader.IsDBNull(6) ? null : reader.GetString(6),
|
||||||
|
NewValue = reader.IsDBNull(7) ? null : reader.GetString(7),
|
||||||
|
IpAddress = reader.IsDBNull(8) ? null : reader.GetString(8),
|
||||||
|
UserAgent = reader.IsDBNull(9) ? null : reader.GetString(9),
|
||||||
|
CorrelationId = reader.IsDBNull(10) ? null : reader.GetString(10),
|
||||||
|
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using StellaOps.Authority.Storage.Postgres.Models;
|
||||||
|
|
||||||
|
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||||
|
|
||||||
|
public interface IAuditRepository
|
||||||
|
{
|
||||||
|
Task<long> CreateAsync(string tenantId, AuditEntity audit, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<AuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<AuditEntity>> GetByUserIdAsync(string tenantId, Guid userId, int limit = 100, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<AuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId, int limit = 100, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<AuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<AuditEntity>> GetByActionAsync(string tenantId, string action, int limit = 100, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using StellaOps.Authority.Storage.Postgres.Models;
|
||||||
|
|
||||||
|
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||||
|
|
||||||
|
public interface ISessionRepository
|
||||||
|
{
|
||||||
|
Task<SessionEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||||
|
Task<SessionEntity?> GetByTokenHashAsync(string sessionTokenHash, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<SessionEntity>> GetByUserIdAsync(string tenantId, Guid userId, bool activeOnly = true, CancellationToken cancellationToken = default);
|
||||||
|
Task<Guid> CreateAsync(string tenantId, SessionEntity session, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateLastActivityAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||||
|
Task EndAsync(string tenantId, Guid id, string reason, CancellationToken cancellationToken = default);
|
||||||
|
Task EndByUserIdAsync(string tenantId, Guid userId, string reason, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteExpiredAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using StellaOps.Authority.Storage.Postgres.Models;
|
||||||
|
using StellaOps.Infrastructure.Postgres;
|
||||||
|
|
||||||
|
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||||
|
|
||||||
|
public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISessionRepository
|
||||||
|
{
|
||||||
|
public SessionRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||||
|
|
||||||
|
public async Task<SessionEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT id, tenant_id, user_id, session_token_hash, ip_address, user_agent, started_at, last_activity_at, expires_at, ended_at, end_reason, metadata
|
||||||
|
FROM authority.sessions
|
||||||
|
WHERE tenant_id = @tenant_id AND id = @id
|
||||||
|
""";
|
||||||
|
return await QuerySingleOrDefaultAsync(tenantId, sql, MapSession,
|
||||||
|
cmd => { cmd.Parameters.AddWithValue("id", id); },
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SessionEntity?> GetByTokenHashAsync(string sessionTokenHash, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT id, tenant_id, user_id, session_token_hash, ip_address, user_agent, started_at, last_activity_at, expires_at, ended_at, end_reason, metadata
|
||||||
|
FROM authority.sessions
|
||||||
|
WHERE session_token_hash = @session_token_hash AND ended_at IS NULL AND expires_at > NOW()
|
||||||
|
""";
|
||||||
|
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = sql;
|
||||||
|
command.Parameters.AddWithValue("session_token_hash", sessionTokenHash);
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false) ? MapSession(reader) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SessionEntity>> GetByUserIdAsync(string tenantId, Guid userId, bool activeOnly = true, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var sql = $"""
|
||||||
|
SELECT id, tenant_id, user_id, session_token_hash, ip_address, user_agent, started_at, last_activity_at, expires_at, ended_at, end_reason, metadata
|
||||||
|
FROM authority.sessions
|
||||||
|
WHERE tenant_id = @tenant_id AND user_id = @user_id
|
||||||
|
{(activeOnly ? "AND ended_at IS NULL AND expires_at > NOW()" : "")}
|
||||||
|
ORDER BY started_at DESC
|
||||||
|
""";
|
||||||
|
return await QueryAsync(tenantId, sql, MapSession,
|
||||||
|
cmd => { cmd.Parameters.AddWithValue("user_id", userId); },
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid> CreateAsync(string tenantId, SessionEntity session, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO authority.sessions (id, tenant_id, user_id, session_token_hash, ip_address, user_agent, expires_at, metadata)
|
||||||
|
VALUES (@id, @tenant_id, @user_id, @session_token_hash, @ip_address, @user_agent, @expires_at, @metadata::jsonb)
|
||||||
|
RETURNING id
|
||||||
|
""";
|
||||||
|
var id = session.Id == Guid.Empty ? Guid.NewGuid() : session.Id;
|
||||||
|
await ExecuteAsync(tenantId, sql, cmd =>
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("id", id);
|
||||||
|
cmd.Parameters.AddWithValue("user_id", session.UserId);
|
||||||
|
cmd.Parameters.AddWithValue("session_token_hash", session.SessionTokenHash);
|
||||||
|
AddNullableParameter(cmd, "ip_address", session.IpAddress);
|
||||||
|
AddNullableParameter(cmd, "user_agent", session.UserAgent);
|
||||||
|
cmd.Parameters.AddWithValue("expires_at", session.ExpiresAt);
|
||||||
|
AddJsonbParameter(cmd, "metadata", session.Metadata);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLastActivityAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = "UPDATE authority.sessions SET last_activity_at = NOW() WHERE tenant_id = @tenant_id AND id = @id AND ended_at IS NULL";
|
||||||
|
await ExecuteAsync(tenantId, sql, cmd => { cmd.Parameters.AddWithValue("id", id); }, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EndAsync(string tenantId, Guid id, string reason, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
UPDATE authority.sessions SET ended_at = NOW(), end_reason = @end_reason
|
||||||
|
WHERE tenant_id = @tenant_id AND id = @id AND ended_at IS NULL
|
||||||
|
""";
|
||||||
|
await ExecuteAsync(tenantId, sql, cmd =>
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("id", id);
|
||||||
|
cmd.Parameters.AddWithValue("end_reason", reason);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EndByUserIdAsync(string tenantId, Guid userId, string reason, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
UPDATE authority.sessions SET ended_at = NOW(), end_reason = @end_reason
|
||||||
|
WHERE tenant_id = @tenant_id AND user_id = @user_id AND ended_at IS NULL
|
||||||
|
""";
|
||||||
|
await ExecuteAsync(tenantId, sql, cmd =>
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("user_id", userId);
|
||||||
|
cmd.Parameters.AddWithValue("end_reason", reason);
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteExpiredAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = "DELETE FROM authority.sessions WHERE expires_at < NOW() - INTERVAL '30 days'";
|
||||||
|
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = sql;
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SessionEntity MapSession(System.Data.Common.DbDataReader reader) => new()
|
||||||
|
{
|
||||||
|
Id = reader.GetGuid(0),
|
||||||
|
TenantId = reader.GetString(1),
|
||||||
|
UserId = reader.GetGuid(2),
|
||||||
|
SessionTokenHash = reader.GetString(3),
|
||||||
|
IpAddress = reader.IsDBNull(4) ? null : reader.GetString(4),
|
||||||
|
UserAgent = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||||
|
StartedAt = reader.GetFieldValue<DateTimeOffset>(6),
|
||||||
|
LastActivityAt = reader.GetFieldValue<DateTimeOffset>(7),
|
||||||
|
ExpiresAt = reader.GetFieldValue<DateTimeOffset>(8),
|
||||||
|
EndedAt = reader.IsDBNull(9) ? null : reader.GetFieldValue<DateTimeOffset>(9),
|
||||||
|
EndReason = reader.IsDBNull(10) ? null : reader.GetString(10),
|
||||||
|
Metadata = reader.GetString(11)
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user