up
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for API key operations.
|
||||
/// </summary>
|
||||
public sealed class ApiKeyRepository : RepositoryBase<AuthorityDataSource>, IApiKeyRepository
|
||||
{
|
||||
public ApiKeyRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||
public ApiKeyRepository(AuthorityDataSource dataSource, ILogger<ApiKeyRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<ApiKeyEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -14,9 +20,9 @@ public sealed class ApiKeyRepository : RepositoryBase<AuthorityDataSource>, IApi
|
||||
FROM authority.api_keys
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, MapApiKey,
|
||||
cmd => { cmd.Parameters.AddWithValue("id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapApiKey, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ApiKeyEntity?> GetByPrefixAsync(string keyPrefix, CancellationToken cancellationToken = default)
|
||||
@@ -27,9 +33,8 @@ public sealed class ApiKeyRepository : RepositoryBase<AuthorityDataSource>, IApi
|
||||
WHERE key_prefix = @key_prefix AND status = 'active'
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
command.Parameters.AddWithValue("key_prefix", keyPrefix);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "key_prefix", keyPrefix);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false) ? MapApiKey(reader) : null;
|
||||
}
|
||||
@@ -42,7 +47,9 @@ public sealed class ApiKeyRepository : RepositoryBase<AuthorityDataSource>, IApi
|
||||
WHERE tenant_id = @tenant_id
|
||||
ORDER BY created_at DESC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapApiKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapApiKey, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ApiKeyEntity>> GetByUserIdAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default)
|
||||
@@ -53,9 +60,9 @@ public sealed class ApiKeyRepository : RepositoryBase<AuthorityDataSource>, IApi
|
||||
WHERE tenant_id = @tenant_id AND user_id = @user_id
|
||||
ORDER BY created_at DESC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapApiKey,
|
||||
cmd => { cmd.Parameters.AddWithValue("user_id", userId); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
MapApiKey, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateAsync(string tenantId, ApiKeyEntity apiKey, CancellationToken cancellationToken = default)
|
||||
@@ -66,25 +73,28 @@ public sealed class ApiKeyRepository : RepositoryBase<AuthorityDataSource>, IApi
|
||||
RETURNING id
|
||||
""";
|
||||
var id = apiKey.Id == Guid.Empty ? Guid.NewGuid() : apiKey.Id;
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
AddNullableParameter(cmd, "user_id", apiKey.UserId);
|
||||
cmd.Parameters.AddWithValue("name", apiKey.Name);
|
||||
cmd.Parameters.AddWithValue("key_hash", apiKey.KeyHash);
|
||||
cmd.Parameters.AddWithValue("key_prefix", apiKey.KeyPrefix);
|
||||
AddArrayParameter(cmd, "scopes", apiKey.Scopes);
|
||||
cmd.Parameters.AddWithValue("status", apiKey.Status);
|
||||
AddNullableParameter(cmd, "expires_at", apiKey.ExpiresAt);
|
||||
AddJsonbParameter(cmd, "metadata", apiKey.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "user_id", apiKey.UserId);
|
||||
AddParameter(command, "name", apiKey.Name);
|
||||
AddParameter(command, "key_hash", apiKey.KeyHash);
|
||||
AddParameter(command, "key_prefix", apiKey.KeyPrefix);
|
||||
AddTextArrayParameter(command, "scopes", apiKey.Scopes);
|
||||
AddParameter(command, "status", apiKey.Status);
|
||||
AddParameter(command, "expires_at", apiKey.ExpiresAt);
|
||||
AddJsonbParameter(command, "metadata", apiKey.Metadata);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return id;
|
||||
}
|
||||
|
||||
public async Task UpdateLastUsedAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE authority.api_keys SET last_used_at = NOW() WHERE tenant_id = @tenant_id AND id = @id";
|
||||
await ExecuteAsync(tenantId, sql, cmd => { cmd.Parameters.AddWithValue("id", id); }, cancellationToken).ConfigureAwait(false);
|
||||
await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task RevokeAsync(string tenantId, Guid id, string revokedBy, CancellationToken cancellationToken = default)
|
||||
@@ -95,32 +105,35 @@ public sealed class ApiKeyRepository : RepositoryBase<AuthorityDataSource>, IApi
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
cmd.Parameters.AddWithValue("revoked_by", revokedBy);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "revoked_by", revokedBy);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM authority.api_keys WHERE tenant_id = @tenant_id AND id = @id";
|
||||
await ExecuteAsync(tenantId, sql, cmd => { cmd.Parameters.AddWithValue("id", id); }, cancellationToken).ConfigureAwait(false);
|
||||
await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static ApiKeyEntity MapApiKey(System.Data.Common.DbDataReader reader) => new()
|
||||
private static ApiKeyEntity MapApiKey(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = reader.IsDBNull(2) ? null : reader.GetGuid(2),
|
||||
UserId = GetNullableGuid(reader, 2),
|
||||
Name = reader.GetString(3),
|
||||
KeyHash = reader.GetString(4),
|
||||
KeyPrefix = reader.GetString(5),
|
||||
Scopes = reader.IsDBNull(6) ? [] : reader.GetFieldValue<string[]>(6),
|
||||
Status = reader.GetString(7),
|
||||
LastUsedAt = reader.IsDBNull(8) ? null : reader.GetFieldValue<DateTimeOffset>(8),
|
||||
ExpiresAt = reader.IsDBNull(9) ? null : reader.GetFieldValue<DateTimeOffset>(9),
|
||||
LastUsedAt = GetNullableDateTimeOffset(reader, 8),
|
||||
ExpiresAt = GetNullableDateTimeOffset(reader, 9),
|
||||
Metadata = reader.GetString(10),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11),
|
||||
RevokedAt = reader.IsDBNull(12) ? null : reader.GetFieldValue<DateTimeOffset>(12),
|
||||
RevokedBy = reader.IsDBNull(13) ? null : reader.GetString(13)
|
||||
RevokedAt = GetNullableDateTimeOffset(reader, 12),
|
||||
RevokedBy = GetNullableString(reader, 13)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for audit log operations.
|
||||
/// </summary>
|
||||
public sealed class AuditRepository : RepositoryBase<AuthorityDataSource>, IAuditRepository
|
||||
{
|
||||
public AuditRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||
public AuditRepository(AuthorityDataSource dataSource, ILogger<AuditRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<long> CreateAsync(string tenantId, AuditEntity audit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -14,19 +20,18 @@ public sealed class AuditRepository : RepositoryBase<AuthorityDataSource>, IAudi
|
||||
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);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "user_id", audit.UserId);
|
||||
AddParameter(command, "action", audit.Action);
|
||||
AddParameter(command, "resource_type", audit.ResourceType);
|
||||
AddParameter(command, "resource_id", audit.ResourceId);
|
||||
AddJsonbParameter(command, "old_value", audit.OldValue);
|
||||
AddJsonbParameter(command, "new_value", audit.NewValue);
|
||||
AddParameter(command, "ip_address", audit.IpAddress);
|
||||
AddParameter(command, "user_agent", audit.UserAgent);
|
||||
AddParameter(command, "correlation_id", audit.CorrelationId);
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return (long)result!;
|
||||
}
|
||||
@@ -40,11 +45,12 @@ public sealed class AuditRepository : RepositoryBase<AuthorityDataSource>, IAudi
|
||||
ORDER BY created_at DESC
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapAudit, cmd =>
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("limit", limit);
|
||||
cmd.Parameters.AddWithValue("offset", offset);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AuditEntity>> GetByUserIdAsync(string tenantId, Guid userId, int limit = 100, CancellationToken cancellationToken = default)
|
||||
@@ -56,29 +62,31 @@ public sealed class AuditRepository : RepositoryBase<AuthorityDataSource>, IAudi
|
||||
ORDER BY created_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapAudit, cmd =>
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("user_id", userId);
|
||||
cmd.Parameters.AddWithValue("limit", limit);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "user_id", userId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId, int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
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 =>
|
||||
if (resourceId != null) sql += " AND resource_id = @resource_id";
|
||||
sql += " ORDER BY created_at DESC LIMIT @limit";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("resource_type", resourceType);
|
||||
if (resourceId != null) cmd.Parameters.AddWithValue("resource_id", resourceId);
|
||||
cmd.Parameters.AddWithValue("limit", limit);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "resource_type", resourceType);
|
||||
if (resourceId != null) AddParameter(cmd, "resource_id", resourceId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default)
|
||||
@@ -89,9 +97,11 @@ public sealed class AuditRepository : RepositoryBase<AuthorityDataSource>, IAudi
|
||||
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);
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "correlation_id", correlationId);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AuditEntity>> GetByActionAsync(string tenantId, string action, int limit = 100, CancellationToken cancellationToken = default)
|
||||
@@ -103,34 +113,27 @@ public sealed class AuditRepository : RepositoryBase<AuthorityDataSource>, IAudi
|
||||
ORDER BY created_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapAudit, cmd =>
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("action", action);
|
||||
cmd.Parameters.AddWithValue("limit", limit);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "action", action);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapAudit, 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()
|
||||
private static AuditEntity MapAudit(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetInt64(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = reader.IsDBNull(2) ? null : reader.GetGuid(2),
|
||||
UserId = GetNullableGuid(reader, 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),
|
||||
ResourceId = GetNullableString(reader, 5),
|
||||
OldValue = GetNullableString(reader, 6),
|
||||
NewValue = GetNullableString(reader, 7),
|
||||
IpAddress = GetNullableString(reader, 8),
|
||||
UserAgent = GetNullableString(reader, 9),
|
||||
CorrelationId = GetNullableString(reader, 10),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for permission operations.
|
||||
/// </summary>
|
||||
public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>, IPermissionRepository
|
||||
{
|
||||
public PermissionRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||
public PermissionRepository(AuthorityDataSource dataSource, ILogger<PermissionRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<PermissionEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -14,9 +20,9 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
FROM authority.permissions
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, MapPermission,
|
||||
cmd => { cmd.Parameters.AddWithValue("id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapPermission, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<PermissionEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default)
|
||||
@@ -26,9 +32,9 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
FROM authority.permissions
|
||||
WHERE tenant_id = @tenant_id AND name = @name
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, MapPermission,
|
||||
cmd => { cmd.Parameters.AddWithValue("name", name); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "name", name); },
|
||||
MapPermission, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PermissionEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
@@ -39,7 +45,9 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
WHERE tenant_id = @tenant_id
|
||||
ORDER BY resource, action
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapPermission, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapPermission, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PermissionEntity>> GetByResourceAsync(string tenantId, string resource, CancellationToken cancellationToken = default)
|
||||
@@ -50,9 +58,9 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
WHERE tenant_id = @tenant_id AND resource = @resource
|
||||
ORDER BY action
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapPermission,
|
||||
cmd => { cmd.Parameters.AddWithValue("resource", resource); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "resource", resource); },
|
||||
MapPermission, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PermissionEntity>> GetRolePermissionsAsync(string tenantId, Guid roleId, CancellationToken cancellationToken = default)
|
||||
@@ -64,9 +72,9 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
WHERE p.tenant_id = @tenant_id AND rp.role_id = @role_id
|
||||
ORDER BY p.resource, p.action
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapPermission,
|
||||
cmd => { cmd.Parameters.AddWithValue("role_id", roleId); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "role_id", roleId); },
|
||||
MapPermission, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PermissionEntity>> GetUserPermissionsAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default)
|
||||
@@ -80,9 +88,9 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
AND (ur.expires_at IS NULL OR ur.expires_at > NOW())
|
||||
ORDER BY p.resource, p.action
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapPermission,
|
||||
cmd => { cmd.Parameters.AddWithValue("user_id", userId); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
MapPermission, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateAsync(string tenantId, PermissionEntity permission, CancellationToken cancellationToken = default)
|
||||
@@ -93,21 +101,24 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
RETURNING id
|
||||
""";
|
||||
var id = permission.Id == Guid.Empty ? Guid.NewGuid() : permission.Id;
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
cmd.Parameters.AddWithValue("name", permission.Name);
|
||||
cmd.Parameters.AddWithValue("resource", permission.Resource);
|
||||
cmd.Parameters.AddWithValue("action", permission.Action);
|
||||
AddNullableParameter(cmd, "description", permission.Description);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "name", permission.Name);
|
||||
AddParameter(command, "resource", permission.Resource);
|
||||
AddParameter(command, "action", permission.Action);
|
||||
AddParameter(command, "description", permission.Description);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return id;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM authority.permissions WHERE tenant_id = @tenant_id AND id = @id";
|
||||
await ExecuteAsync(tenantId, sql, cmd => { cmd.Parameters.AddWithValue("id", id); }, cancellationToken).ConfigureAwait(false);
|
||||
await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task AssignToRoleAsync(string tenantId, Guid roleId, Guid permissionId, CancellationToken cancellationToken = default)
|
||||
@@ -119,8 +130,8 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("role_id", roleId);
|
||||
cmd.Parameters.AddWithValue("permission_id", permissionId);
|
||||
AddParameter(cmd, "role_id", roleId);
|
||||
AddParameter(cmd, "permission_id", permissionId);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -129,19 +140,19 @@ public sealed class PermissionRepository : RepositoryBase<AuthorityDataSource>,
|
||||
const string sql = "DELETE FROM authority.role_permissions WHERE role_id = @role_id AND permission_id = @permission_id";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("role_id", roleId);
|
||||
cmd.Parameters.AddWithValue("permission_id", permissionId);
|
||||
AddParameter(cmd, "role_id", roleId);
|
||||
AddParameter(cmd, "permission_id", permissionId);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static PermissionEntity MapPermission(System.Data.Common.DbDataReader reader) => new()
|
||||
private static PermissionEntity MapPermission(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
Resource = reader.GetString(3),
|
||||
Action = reader.GetString(4),
|
||||
Description = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
Description = GetNullableString(reader, 5),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(6)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for role operations.
|
||||
/// </summary>
|
||||
public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleRepository
|
||||
{
|
||||
public RoleRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||
public RoleRepository(AuthorityDataSource dataSource, ILogger<RoleRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<RoleEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -14,9 +20,9 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
FROM authority.roles
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, MapRole,
|
||||
cmd => { cmd.Parameters.AddWithValue("id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapRole, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<RoleEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default)
|
||||
@@ -26,9 +32,9 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
FROM authority.roles
|
||||
WHERE tenant_id = @tenant_id AND name = @name
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, MapRole,
|
||||
cmd => { cmd.Parameters.AddWithValue("name", name); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "name", name); },
|
||||
MapRole, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RoleEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
@@ -39,7 +45,9 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
WHERE tenant_id = @tenant_id
|
||||
ORDER BY name
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapRole, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapRole, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RoleEntity>> GetUserRolesAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default)
|
||||
@@ -52,9 +60,9 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
AND (ur.expires_at IS NULL OR ur.expires_at > NOW())
|
||||
ORDER BY r.name
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapRole,
|
||||
cmd => { cmd.Parameters.AddWithValue("user_id", userId); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
MapRole, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateAsync(string tenantId, RoleEntity role, CancellationToken cancellationToken = default)
|
||||
@@ -65,15 +73,16 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
RETURNING id
|
||||
""";
|
||||
var id = role.Id == Guid.Empty ? Guid.NewGuid() : role.Id;
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
cmd.Parameters.AddWithValue("name", role.Name);
|
||||
AddNullableParameter(cmd, "display_name", role.DisplayName);
|
||||
AddNullableParameter(cmd, "description", role.Description);
|
||||
cmd.Parameters.AddWithValue("is_system", role.IsSystem);
|
||||
AddJsonbParameter(cmd, "metadata", role.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "name", role.Name);
|
||||
AddParameter(command, "display_name", role.DisplayName);
|
||||
AddParameter(command, "description", role.Description);
|
||||
AddParameter(command, "is_system", role.IsSystem);
|
||||
AddJsonbParameter(command, "metadata", role.Metadata);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -87,11 +96,12 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", role.Id);
|
||||
cmd.Parameters.AddWithValue("name", role.Name);
|
||||
AddNullableParameter(cmd, "display_name", role.DisplayName);
|
||||
AddNullableParameter(cmd, "description", role.Description);
|
||||
cmd.Parameters.AddWithValue("is_system", role.IsSystem);
|
||||
AddParameter(cmd, "id", role.Id);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "name", role.Name);
|
||||
AddParameter(cmd, "display_name", role.DisplayName);
|
||||
AddParameter(cmd, "description", role.Description);
|
||||
AddParameter(cmd, "is_system", role.IsSystem);
|
||||
AddJsonbParameter(cmd, "metadata", role.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -99,7 +109,9 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
public async Task DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM authority.roles WHERE tenant_id = @tenant_id AND id = @id";
|
||||
await ExecuteAsync(tenantId, sql, cmd => { cmd.Parameters.AddWithValue("id", id); }, cancellationToken).ConfigureAwait(false);
|
||||
await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task AssignToUserAsync(string tenantId, Guid userId, Guid roleId, string? grantedBy, DateTimeOffset? expiresAt, CancellationToken cancellationToken = default)
|
||||
@@ -112,10 +124,10 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("user_id", userId);
|
||||
cmd.Parameters.AddWithValue("role_id", roleId);
|
||||
AddNullableParameter(cmd, "granted_by", grantedBy);
|
||||
AddNullableParameter(cmd, "expires_at", expiresAt);
|
||||
AddParameter(cmd, "user_id", userId);
|
||||
AddParameter(cmd, "role_id", roleId);
|
||||
AddParameter(cmd, "granted_by", grantedBy);
|
||||
AddParameter(cmd, "expires_at", expiresAt);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -124,18 +136,18 @@ public sealed class RoleRepository : RepositoryBase<AuthorityDataSource>, IRoleR
|
||||
const string sql = "DELETE FROM authority.user_roles WHERE user_id = @user_id AND role_id = @role_id";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("user_id", userId);
|
||||
cmd.Parameters.AddWithValue("role_id", roleId);
|
||||
AddParameter(cmd, "user_id", userId);
|
||||
AddParameter(cmd, "role_id", roleId);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static RoleEntity MapRole(System.Data.Common.DbDataReader reader) => new()
|
||||
private static RoleEntity MapRole(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
DisplayName = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
Description = reader.IsDBNull(4) ? null : reader.GetString(4),
|
||||
DisplayName = GetNullableString(reader, 3),
|
||||
Description = GetNullableString(reader, 4),
|
||||
IsSystem = reader.GetBoolean(5),
|
||||
Metadata = reader.GetString(6),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(7),
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for session operations.
|
||||
/// </summary>
|
||||
public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISessionRepository
|
||||
{
|
||||
public SessionRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||
public SessionRepository(AuthorityDataSource dataSource, ILogger<SessionRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<SessionEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -14,9 +20,9 @@ public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISe
|
||||
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);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapSession, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SessionEntity?> GetByTokenHashAsync(string sessionTokenHash, CancellationToken cancellationToken = default)
|
||||
@@ -27,25 +33,25 @@ public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISe
|
||||
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 command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "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 = $"""
|
||||
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);
|
||||
if (activeOnly) sql += " AND ended_at IS NULL AND expires_at > NOW()";
|
||||
sql += " ORDER BY started_at DESC";
|
||||
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
MapSession, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateAsync(string tenantId, SessionEntity session, CancellationToken cancellationToken = default)
|
||||
@@ -56,23 +62,26 @@ public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISe
|
||||
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);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "user_id", session.UserId);
|
||||
AddParameter(command, "session_token_hash", session.SessionTokenHash);
|
||||
AddParameter(command, "ip_address", session.IpAddress);
|
||||
AddParameter(command, "user_agent", session.UserAgent);
|
||||
AddParameter(command, "expires_at", session.ExpiresAt);
|
||||
AddJsonbParameter(command, "metadata", session.Metadata);
|
||||
await command.ExecuteNonQueryAsync(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);
|
||||
await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task EndAsync(string tenantId, Guid id, string reason, CancellationToken cancellationToken = default)
|
||||
@@ -83,8 +92,9 @@ public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISe
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
cmd.Parameters.AddWithValue("end_reason", reason);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "end_reason", reason);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -96,8 +106,9 @@ public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISe
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("user_id", userId);
|
||||
cmd.Parameters.AddWithValue("end_reason", reason);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "user_id", userId);
|
||||
AddParameter(cmd, "end_reason", reason);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -105,24 +116,23 @@ public sealed class SessionRepository : RepositoryBase<AuthorityDataSource>, ISe
|
||||
{
|
||||
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 using var command = CreateCommand(sql, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static SessionEntity MapSession(System.Data.Common.DbDataReader reader) => new()
|
||||
private static SessionEntity MapSession(NpgsqlDataReader 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),
|
||||
IpAddress = GetNullableString(reader, 4),
|
||||
UserAgent = GetNullableString(reader, 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),
|
||||
EndedAt = GetNullableDateTimeOffset(reader, 9),
|
||||
EndReason = GetNullableString(reader, 10),
|
||||
Metadata = reader.GetString(11)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for access token operations.
|
||||
/// </summary>
|
||||
public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, ITokenRepository
|
||||
{
|
||||
public TokenRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||
public TokenRepository(AuthorityDataSource dataSource, ILogger<TokenRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<TokenEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -14,9 +20,9 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
FROM authority.tokens
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, MapToken,
|
||||
cmd => { cmd.Parameters.AddWithValue("id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapToken, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TokenEntity?> GetByHashAsync(string tokenHash, CancellationToken cancellationToken = default)
|
||||
@@ -27,9 +33,8 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
WHERE token_hash = @token_hash AND revoked_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("token_hash", tokenHash);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "token_hash", tokenHash);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false) ? MapToken(reader) : null;
|
||||
}
|
||||
@@ -42,9 +47,9 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
WHERE tenant_id = @tenant_id AND user_id = @user_id AND revoked_at IS NULL
|
||||
ORDER BY issued_at DESC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapToken,
|
||||
cmd => { cmd.Parameters.AddWithValue("user_id", userId); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
MapToken, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateAsync(string tenantId, TokenEntity token, CancellationToken cancellationToken = default)
|
||||
@@ -55,17 +60,18 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
RETURNING id
|
||||
""";
|
||||
var id = token.Id == Guid.Empty ? Guid.NewGuid() : token.Id;
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
AddNullableParameter(cmd, "user_id", token.UserId);
|
||||
cmd.Parameters.AddWithValue("token_hash", token.TokenHash);
|
||||
cmd.Parameters.AddWithValue("token_type", token.TokenType);
|
||||
AddArrayParameter(cmd, "scopes", token.Scopes);
|
||||
AddNullableParameter(cmd, "client_id", token.ClientId);
|
||||
cmd.Parameters.AddWithValue("expires_at", token.ExpiresAt);
|
||||
AddJsonbParameter(cmd, "metadata", token.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "user_id", token.UserId);
|
||||
AddParameter(command, "token_hash", token.TokenHash);
|
||||
AddParameter(command, "token_type", token.TokenType);
|
||||
AddTextArrayParameter(command, "scopes", token.Scopes);
|
||||
AddParameter(command, "client_id", token.ClientId);
|
||||
AddParameter(command, "expires_at", token.ExpiresAt);
|
||||
AddJsonbParameter(command, "metadata", token.Metadata);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -77,8 +83,9 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
cmd.Parameters.AddWithValue("revoked_by", revokedBy);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "revoked_by", revokedBy);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -90,8 +97,9 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("user_id", userId);
|
||||
cmd.Parameters.AddWithValue("revoked_by", revokedBy);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "user_id", userId);
|
||||
AddParameter(cmd, "revoked_by", revokedBy);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -99,31 +107,34 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
{
|
||||
const string sql = "DELETE FROM authority.tokens WHERE expires_at < NOW() - INTERVAL '7 days'";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static TokenEntity MapToken(System.Data.Common.DbDataReader reader) => new()
|
||||
private static TokenEntity MapToken(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = reader.IsDBNull(2) ? null : reader.GetGuid(2),
|
||||
UserId = GetNullableGuid(reader, 2),
|
||||
TokenHash = reader.GetString(3),
|
||||
TokenType = reader.GetString(4),
|
||||
Scopes = reader.IsDBNull(5) ? [] : reader.GetFieldValue<string[]>(5),
|
||||
ClientId = reader.IsDBNull(6) ? null : reader.GetString(6),
|
||||
ClientId = GetNullableString(reader, 6),
|
||||
IssuedAt = reader.GetFieldValue<DateTimeOffset>(7),
|
||||
ExpiresAt = reader.GetFieldValue<DateTimeOffset>(8),
|
||||
RevokedAt = reader.IsDBNull(9) ? null : reader.GetFieldValue<DateTimeOffset>(9),
|
||||
RevokedBy = reader.IsDBNull(10) ? null : reader.GetString(10),
|
||||
RevokedAt = GetNullableDateTimeOffset(reader, 9),
|
||||
RevokedBy = GetNullableString(reader, 10),
|
||||
Metadata = reader.GetString(11)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for refresh token operations.
|
||||
/// </summary>
|
||||
public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>, IRefreshTokenRepository
|
||||
{
|
||||
public RefreshTokenRepository(AuthorityDataSource dataSource) : base(dataSource) { }
|
||||
public RefreshTokenRepository(AuthorityDataSource dataSource, ILogger<RefreshTokenRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<RefreshTokenEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -132,9 +143,9 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
FROM authority.refresh_tokens
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, MapRefreshToken,
|
||||
cmd => { cmd.Parameters.AddWithValue("id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapRefreshToken, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<RefreshTokenEntity?> GetByHashAsync(string tokenHash, CancellationToken cancellationToken = default)
|
||||
@@ -145,9 +156,8 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
WHERE token_hash = @token_hash AND revoked_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("token_hash", tokenHash);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "token_hash", tokenHash);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false) ? MapRefreshToken(reader) : null;
|
||||
}
|
||||
@@ -160,9 +170,9 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
WHERE tenant_id = @tenant_id AND user_id = @user_id AND revoked_at IS NULL
|
||||
ORDER BY issued_at DESC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, MapRefreshToken,
|
||||
cmd => { cmd.Parameters.AddWithValue("user_id", userId); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
MapRefreshToken, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateAsync(string tenantId, RefreshTokenEntity token, CancellationToken cancellationToken = default)
|
||||
@@ -173,16 +183,17 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
RETURNING id
|
||||
""";
|
||||
var id = token.Id == Guid.Empty ? Guid.NewGuid() : token.Id;
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
cmd.Parameters.AddWithValue("user_id", token.UserId);
|
||||
cmd.Parameters.AddWithValue("token_hash", token.TokenHash);
|
||||
AddNullableParameter(cmd, "access_token_id", token.AccessTokenId);
|
||||
AddNullableParameter(cmd, "client_id", token.ClientId);
|
||||
cmd.Parameters.AddWithValue("expires_at", token.ExpiresAt);
|
||||
AddJsonbParameter(cmd, "metadata", token.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "user_id", token.UserId);
|
||||
AddParameter(command, "token_hash", token.TokenHash);
|
||||
AddParameter(command, "access_token_id", token.AccessTokenId);
|
||||
AddParameter(command, "client_id", token.ClientId);
|
||||
AddParameter(command, "expires_at", token.ExpiresAt);
|
||||
AddJsonbParameter(command, "metadata", token.Metadata);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -194,9 +205,10 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("id", id);
|
||||
cmd.Parameters.AddWithValue("revoked_by", revokedBy);
|
||||
AddNullableParameter(cmd, "replaced_by", replacedBy);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "revoked_by", revokedBy);
|
||||
AddParameter(cmd, "replaced_by", replacedBy);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -208,8 +220,9 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
""";
|
||||
await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
cmd.Parameters.AddWithValue("user_id", userId);
|
||||
cmd.Parameters.AddWithValue("revoked_by", revokedBy);
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "user_id", userId);
|
||||
AddParameter(cmd, "revoked_by", revokedBy);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -217,24 +230,23 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
{
|
||||
const string sql = "DELETE FROM authority.refresh_tokens 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 using var command = CreateCommand(sql, connection);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static RefreshTokenEntity MapRefreshToken(System.Data.Common.DbDataReader reader) => new()
|
||||
private static RefreshTokenEntity MapRefreshToken(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = reader.GetGuid(2),
|
||||
TokenHash = reader.GetString(3),
|
||||
AccessTokenId = reader.IsDBNull(4) ? null : reader.GetGuid(4),
|
||||
ClientId = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
AccessTokenId = GetNullableGuid(reader, 4),
|
||||
ClientId = GetNullableString(reader, 5),
|
||||
IssuedAt = reader.GetFieldValue<DateTimeOffset>(6),
|
||||
ExpiresAt = reader.GetFieldValue<DateTimeOffset>(7),
|
||||
RevokedAt = reader.IsDBNull(8) ? null : reader.GetFieldValue<DateTimeOffset>(8),
|
||||
RevokedBy = reader.IsDBNull(9) ? null : reader.GetString(9),
|
||||
ReplacedBy = reader.IsDBNull(10) ? null : reader.GetGuid(10),
|
||||
RevokedAt = GetNullableDateTimeOffset(reader, 8),
|
||||
RevokedBy = GetNullableString(reader, 9),
|
||||
ReplacedBy = GetNullableGuid(reader, 10),
|
||||
Metadata = reader.GetString(11)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ public static class ServiceCollectionExtensions
|
||||
// Register repositories
|
||||
services.AddScoped<ITenantRepository, TenantRepository>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IRoleRepository, RoleRepository>();
|
||||
services.AddScoped<IPermissionRepository, PermissionRepository>();
|
||||
services.AddScoped<ITokenRepository, TokenRepository>();
|
||||
services.AddScoped<IRefreshTokenRepository, RefreshTokenRepository>();
|
||||
services.AddScoped<IApiKeyRepository, ApiKeyRepository>();
|
||||
services.AddScoped<ISessionRepository, SessionRepository>();
|
||||
services.AddScoped<IAuditRepository, AuditRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -49,6 +56,13 @@ public static class ServiceCollectionExtensions
|
||||
// Register repositories
|
||||
services.AddScoped<ITenantRepository, TenantRepository>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IRoleRepository, RoleRepository>();
|
||||
services.AddScoped<IPermissionRepository, PermissionRepository>();
|
||||
services.AddScoped<ITokenRepository, TokenRepository>();
|
||||
services.AddScoped<IRefreshTokenRepository, RefreshTokenRepository>();
|
||||
services.AddScoped<IApiKeyRepository, ApiKeyRepository>();
|
||||
services.AddScoped<ISessionRepository, SessionRepository>();
|
||||
services.AddScoped<IAuditRepository, AuditRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user