Working on this data setup logic
This commit is contained in:
parent
384cc3c6fd
commit
447b27f69b
16 changed files with 409 additions and 211 deletions
|
|
@ -16,8 +16,8 @@ namespace Configuration.Core;
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
SELECT id, key, value, label, content_type, valid_from, expires_at, created_at, modified_at, etag
|
SELECT id, key, value, label, content_type, valid_from, expires_at, created_at, modified_at, etag
|
||||||
FROM prod.app_configuration
|
FROM prod.app_configuration
|
||||||
WHERE (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
WHERE (expires_at IS NULL OR expires_at <= CURRENT_TIMESTAMP)
|
||||||
AND (valid_from IS NULL OR valid_from <= CURRENT_TIMESTAMP)";
|
AND (valid_from IS NULL OR valid_from >= CURRENT_TIMESTAMP)";
|
||||||
|
|
||||||
return await _connection.QueryAsync<AppConfiguration>(sql);
|
return await _connection.QueryAsync<AppConfiguration>(sql);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,80 @@
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using System.Data;
|
|
||||||
|
|
||||||
namespace Configuration.Core;
|
namespace Configuration.Core
|
||||||
|
{
|
||||||
public class JsonConfigurationSection : IConfigurationSection
|
public class JsonConfigurationSection : IConfigurationSection
|
||||||
{
|
{
|
||||||
private readonly JObject _data;
|
private readonly JObject _data;
|
||||||
private readonly string _path;
|
private readonly string _path;
|
||||||
|
private readonly string _normalizedPath;
|
||||||
|
|
||||||
public JsonConfigurationSection(JObject data, string path)
|
public JsonConfigurationSection(JObject data, string path)
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data ?? throw new ArgumentNullException(nameof(data));
|
||||||
_path = path;
|
_path = path ?? throw new ArgumentNullException(nameof(path));
|
||||||
|
_normalizedPath = NormalizePath(_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string this[string key]
|
public string this[string key]
|
||||||
{
|
{
|
||||||
get => _data.SelectToken($"{_path.Replace(":", ".")}.{key.Replace(":", ".")}")?.ToString();
|
get
|
||||||
set => throw new NotImplementedException();
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
|
||||||
|
var token = _data.SelectToken($"{_normalizedPath}.{NormalizePath(key)}");
|
||||||
|
return token?.ToString();
|
||||||
|
}
|
||||||
|
set => throw new NotImplementedException("Setting values is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Key => _path.Split(':').Last();
|
public string Key => _path.Split(':').Last();
|
||||||
public string Path => _path;
|
public string Path => _path;
|
||||||
|
|
||||||
public string Value
|
public string Value
|
||||||
{
|
{
|
||||||
get => _data.SelectToken(_path.Replace(":", "."))?.ToString();
|
get
|
||||||
set => throw new NotImplementedException();
|
{
|
||||||
|
var token = _data.SelectToken(_normalizedPath);
|
||||||
|
return token?.ToString();
|
||||||
|
}
|
||||||
|
set => throw new NotImplementedException("Setting values is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IConfigurationSection GetSection(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
|
||||||
public IConfigurationSection GetSection(string key) =>
|
return new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? key : $"{_path}:{key}");
|
||||||
new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? key : $"{_path}:{key}");
|
}
|
||||||
|
|
||||||
public JToken GetToken() => _data.SelectToken(_path.Replace(":", "."));
|
public JToken GetToken() => _data.SelectToken(_normalizedPath);
|
||||||
|
|
||||||
public IEnumerable<IConfigurationSection> GetChildren()
|
public IEnumerable<IConfigurationSection> GetChildren()
|
||||||
{
|
{
|
||||||
var token = _data.SelectToken(_path.Replace(":", "."));
|
var token = _data.SelectToken(_normalizedPath);
|
||||||
if (token is JObject obj)
|
if (token is JObject obj)
|
||||||
{
|
{
|
||||||
return obj.Properties()
|
return obj.Properties()
|
||||||
.Select(p => new JsonConfigurationSection(_data,
|
.Select(p => new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? p.Name : $"{_path}:{p.Name}"));
|
||||||
string.IsNullOrEmpty(_path) ? p.Name : $"{_path}:{p.Name}"));
|
|
||||||
}
|
}
|
||||||
return Enumerable.Empty<IConfigurationSection>();
|
return Enumerable.Empty<IConfigurationSection>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public T Get<T>() where T : class
|
public T Get<T>() where T : class
|
||||||
{
|
{
|
||||||
var token = _data.SelectToken(_path.Replace(":", "."));
|
var token = _data.SelectToken(_normalizedPath);
|
||||||
return token?.ToObject<T>();
|
return token?.ToObject<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IChangeToken GetReloadToken() => new ConfigurationReloadToken();
|
public IChangeToken GetReloadToken() => new ConfigurationReloadToken();
|
||||||
|
|
||||||
|
private static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
return path?.Replace(":", ".", StringComparison.Ordinal) ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,20 +2,27 @@ using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Configuration.Core;
|
namespace Configuration.Core
|
||||||
|
{
|
||||||
public class KeyValueConfigurationBuilder
|
public class KeyValueConfigurationBuilder
|
||||||
{
|
{
|
||||||
private readonly IConfigurationRepository _repository;
|
private readonly IConfigurationRepository _repository;
|
||||||
private readonly JObject _rootObject = new();
|
private readonly JObject _rootObject = new();
|
||||||
private ConfigurationReloadToken _reloadToken = new();
|
private ConfigurationReloadToken _reloadToken = new();
|
||||||
private IConfiguration _configuration;
|
private IConfiguration _configuration;
|
||||||
|
private readonly object _configurationLock = new();
|
||||||
|
|
||||||
public KeyValueConfigurationBuilder(IConfigurationRepository repository)
|
public KeyValueConfigurationBuilder(IConfigurationRepository repository)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads configurations from the repository and builds the configuration tree.
|
||||||
|
/// </summary>
|
||||||
public async Task LoadConfiguration()
|
public async Task LoadConfiguration()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var configurations = await _repository.GetActiveConfigurations();
|
var configurations = await _repository.GetActiveConfigurations();
|
||||||
foreach (var config in configurations)
|
foreach (var config in configurations)
|
||||||
|
|
@ -24,8 +31,26 @@ namespace Configuration.Core;
|
||||||
}
|
}
|
||||||
OnReload();
|
OnReload();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log the exception or handle it as needed
|
||||||
|
throw new InvalidOperationException("Failed to load configurations.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a key-value pair to the configuration tree.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to add.</param>
|
||||||
|
/// <param name="jsonValue">The JSON value to add.</param>
|
||||||
public void AddKeyValue(string key, string jsonValue)
|
public void AddKeyValue(string key, string jsonValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
if (string.IsNullOrEmpty(jsonValue))
|
||||||
|
throw new ArgumentNullException(nameof(jsonValue));
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var valueObject = JsonConvert.DeserializeObject<JObject>(jsonValue);
|
var valueObject = JsonConvert.DeserializeObject<JObject>(jsonValue);
|
||||||
var parts = key.Split(':');
|
var parts = key.Split(':');
|
||||||
|
|
@ -43,14 +68,36 @@ namespace Configuration.Core;
|
||||||
|
|
||||||
current[parts[^1]] = valueObject;
|
current[parts[^1]] = valueObject;
|
||||||
}
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid JSON value.", nameof(jsonValue), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnReload()
|
private void OnReload()
|
||||||
{
|
{
|
||||||
var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
|
var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
|
||||||
previousToken.OnReload();
|
previousToken.OnReload();
|
||||||
_configuration = null;
|
_configuration = null; // Reset the configuration to force a rebuild
|
||||||
}
|
}
|
||||||
|
|
||||||
public IConfiguration Build() =>
|
/// <summary>
|
||||||
_configuration ??= new JsonConfiguration(_rootObject, _reloadToken);
|
/// Builds the configuration instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The built <see cref="IConfiguration"/> instance.</returns>
|
||||||
|
public IConfiguration Build()
|
||||||
|
{
|
||||||
|
if (_configuration == null)
|
||||||
|
{
|
||||||
|
lock (_configurationLock)
|
||||||
|
{
|
||||||
|
if (_configuration == null)
|
||||||
|
{
|
||||||
|
_configuration = new JsonConfiguration(_rootObject, _reloadToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _configuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,11 +11,20 @@ public class ConfigurationDatabaseSetup
|
||||||
{
|
{
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
}
|
}
|
||||||
|
public async Task CreateDatabaseStructure(IDbConnection connection)
|
||||||
|
{
|
||||||
|
await CreateConfigurationTable();
|
||||||
|
await CreateHistoryTable();
|
||||||
|
await CreateConfigurationIndexes();
|
||||||
|
await CreateModifiedAtTrigger();
|
||||||
|
await CreateNotifyTrigger();
|
||||||
|
await CreateHistoryTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CreateConfigurationTable()
|
public async Task CreateConfigurationTable()
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
CREATE TABLE prod.app_configuration (
|
CREATE TABLE app_configuration (
|
||||||
id bigserial NOT NULL,
|
id bigserial NOT NULL,
|
||||||
""key"" varchar(255) NOT NULL,
|
""key"" varchar(255) NOT NULL,
|
||||||
value text NULL,
|
value text NULL,
|
||||||
|
|
@ -34,7 +43,7 @@ public class ConfigurationDatabaseSetup
|
||||||
public async Task CreateHistoryTable()
|
public async Task CreateHistoryTable()
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
CREATE TABLE prod.app_configuration_history (
|
CREATE TABLE app_configuration_history (
|
||||||
history_id bigserial NOT NULL,
|
history_id bigserial NOT NULL,
|
||||||
action_type char(1) NOT NULL,
|
action_type char(1) NOT NULL,
|
||||||
action_timestamp timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
action_timestamp timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
@ -57,15 +66,15 @@ public class ConfigurationDatabaseSetup
|
||||||
public async Task CreateConfigurationIndexes()
|
public async Task CreateConfigurationIndexes()
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
CREATE INDEX idx_app_configuration_key ON prod.app_configuration(""key"");
|
CREATE INDEX idx_app_configuration_key ON app_configuration(""key"");
|
||||||
CREATE INDEX idx_app_configuration_validity ON prod.app_configuration(valid_from, expires_at);";
|
CREATE INDEX idx_app_configuration_validity ON app_configuration(valid_from, expires_at);";
|
||||||
await _connection.ExecuteAsync(sql);
|
await _connection.ExecuteAsync(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateModifiedAtTrigger()
|
public async Task CreateModifiedAtTrigger()
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
CREATE OR REPLACE FUNCTION prod.update_app_configuration_modified_at()
|
CREATE OR REPLACE FUNCTION update_app_configuration_modified_at()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
NEW.modified_at = CURRENT_TIMESTAMP;
|
NEW.modified_at = CURRENT_TIMESTAMP;
|
||||||
|
|
@ -74,16 +83,16 @@ public class ConfigurationDatabaseSetup
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
CREATE TRIGGER trg_app_configuration_modified_at
|
CREATE TRIGGER trg_app_configuration_modified_at
|
||||||
BEFORE UPDATE ON prod.app_configuration
|
BEFORE UPDATE ON app_configuration
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION prod.update_app_configuration_modified_at();";
|
EXECUTE FUNCTION update_app_configuration_modified_at();";
|
||||||
await _connection.ExecuteAsync(sql);
|
await _connection.ExecuteAsync(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateNotifyTrigger()
|
public async Task CreateNotifyTrigger()
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
CREATE OR REPLACE FUNCTION prod.notify_app_configuration_change()
|
CREATE OR REPLACE FUNCTION notify_app_configuration_change()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
PERFORM pg_notify('config_changes', NEW.key);
|
PERFORM pg_notify('config_changes', NEW.key);
|
||||||
|
|
@ -92,20 +101,20 @@ public class ConfigurationDatabaseSetup
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
CREATE TRIGGER trg_app_configuration_notify
|
CREATE TRIGGER trg_app_configuration_notify
|
||||||
AFTER INSERT OR UPDATE ON prod.app_configuration
|
AFTER INSERT OR UPDATE ON app_configuration
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION prod.notify_app_configuration_change();";
|
EXECUTE FUNCTION notify_app_configuration_change();";
|
||||||
await _connection.ExecuteAsync(sql);
|
await _connection.ExecuteAsync(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateHistoryTrigger()
|
public async Task CreateHistoryTrigger()
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
CREATE OR REPLACE FUNCTION prod.log_app_configuration_changes()
|
CREATE OR REPLACE FUNCTION log_app_configuration_changes()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF (TG_OP = 'INSERT') THEN
|
IF (TG_OP = 'INSERT') THEN
|
||||||
INSERT INTO prod.app_configuration_history (
|
INSERT INTO app_configuration_history (
|
||||||
action_type, id, ""key"", value, label, content_type,
|
action_type, id, ""key"", value, label, content_type,
|
||||||
valid_from, expires_at, created_at, modified_at, etag
|
valid_from, expires_at, created_at, modified_at, etag
|
||||||
)
|
)
|
||||||
|
|
@ -114,7 +123,7 @@ public class ConfigurationDatabaseSetup
|
||||||
NEW.valid_from, NEW.expires_at, NEW.created_at, NEW.modified_at, NEW.etag
|
NEW.valid_from, NEW.expires_at, NEW.created_at, NEW.modified_at, NEW.etag
|
||||||
);
|
);
|
||||||
ELSIF (TG_OP = 'UPDATE') THEN
|
ELSIF (TG_OP = 'UPDATE') THEN
|
||||||
INSERT INTO prod.app_configuration_history (
|
INSERT INTO app_configuration_history (
|
||||||
action_type, id, ""key"", value, label, content_type,
|
action_type, id, ""key"", value, label, content_type,
|
||||||
valid_from, expires_at, created_at, modified_at, etag
|
valid_from, expires_at, created_at, modified_at, etag
|
||||||
)
|
)
|
||||||
|
|
@ -123,7 +132,7 @@ public class ConfigurationDatabaseSetup
|
||||||
OLD.valid_from, OLD.expires_at, OLD.created_at, OLD.modified_at, OLD.etag
|
OLD.valid_from, OLD.expires_at, OLD.created_at, OLD.modified_at, OLD.etag
|
||||||
);
|
);
|
||||||
ELSIF (TG_OP = 'DELETE') THEN
|
ELSIF (TG_OP = 'DELETE') THEN
|
||||||
INSERT INTO prod.app_configuration_history (
|
INSERT INTO app_configuration_history (
|
||||||
action_type, id, ""key"", value, label, content_type,
|
action_type, id, ""key"", value, label, content_type,
|
||||||
valid_from, expires_at, created_at, modified_at, etag
|
valid_from, expires_at, created_at, modified_at, etag
|
||||||
)
|
)
|
||||||
|
|
@ -137,18 +146,10 @@ public class ConfigurationDatabaseSetup
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
CREATE TRIGGER trg_app_configuration_history
|
CREATE TRIGGER trg_app_configuration_history
|
||||||
AFTER INSERT OR UPDATE OR DELETE ON prod.app_configuration
|
AFTER INSERT OR UPDATE OR DELETE ON app_configuration
|
||||||
FOR EACH ROW EXECUTE FUNCTION prod.log_app_configuration_changes();";
|
FOR EACH ROW EXECUTE FUNCTION log_app_configuration_changes();";
|
||||||
await _connection.ExecuteAsync(sql);
|
await _connection.ExecuteAsync(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateDatabaseStructure(IDbConnection connection)
|
|
||||||
{
|
|
||||||
await CreateConfigurationTable();
|
|
||||||
await CreateHistoryTable();
|
|
||||||
await CreateConfigurationIndexes();
|
|
||||||
await CreateModifiedAtTrigger();
|
|
||||||
await CreateNotifyTrigger();
|
|
||||||
await CreateHistoryTrigger();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -9,4 +9,9 @@
|
||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="AuditSystem\" />
|
||||||
|
<Folder Include="NavigationSystem\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
115
Database/IdentitySystem/Setup.cs
Normal file
115
Database/IdentitySystem/Setup.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Database.Tenants
|
||||||
|
{
|
||||||
|
public class DbSetup
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _db;
|
||||||
|
|
||||||
|
public DbSetup(IDbConnection db)
|
||||||
|
{
|
||||||
|
_db = db ?? throw new ArgumentNullException(nameof(db));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the users table in the ptmain schema.
|
||||||
|
/// </summary>
|
||||||
|
public void CreateUsersTable()
|
||||||
|
{
|
||||||
|
ExecuteInTransaction(@"
|
||||||
|
CREATE TABLE IF NOT EXISTS ptmain.users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
email VARCHAR(256) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(256) NOT NULL,
|
||||||
|
security_stamp VARCHAR(36) NOT NULL,
|
||||||
|
email_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
access_failed_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
lockout_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
lockout_end TIMESTAMPTZ NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_login_at TIMESTAMPTZ NULL
|
||||||
|
);");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the tenants table in the ptmain schema.
|
||||||
|
/// </summary>
|
||||||
|
public void CreateTenantsTable()
|
||||||
|
{
|
||||||
|
ExecuteInTransaction(@"
|
||||||
|
CREATE TABLE IF NOT EXISTS ptmain.tenants (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
connection_string VARCHAR(500) NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_by INTEGER NOT NULL REFERENCES ptmain.users(id),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the user_tenants table in the ptmain schema.
|
||||||
|
/// </summary>
|
||||||
|
public void CreateUserTenantsTable()
|
||||||
|
{
|
||||||
|
ExecuteInTransaction(@"
|
||||||
|
CREATE TABLE IF NOT EXISTS ptmain.user_tenants (
|
||||||
|
user_id INTEGER NOT NULL REFERENCES ptmain.users(id),
|
||||||
|
tenant_id INTEGER NOT NULL REFERENCES ptmain.tenants(id),
|
||||||
|
pin_code VARCHAR(10) NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, tenant_id)
|
||||||
|
);");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up Row Level Security (RLS) for the tenants and user_tenants tables.
|
||||||
|
/// </summary>
|
||||||
|
public void SetupRLS()
|
||||||
|
{
|
||||||
|
ExecuteInTransaction(
|
||||||
|
"ALTER TABLE ptmain.tenants ENABLE ROW LEVEL SECURITY;",
|
||||||
|
"ALTER TABLE ptmain.user_tenants ENABLE ROW LEVEL SECURITY;",
|
||||||
|
"DROP POLICY IF EXISTS tenant_access ON ptmain.tenants;",
|
||||||
|
@"
|
||||||
|
CREATE POLICY tenant_access ON ptmain.tenants
|
||||||
|
USING (id IN (
|
||||||
|
SELECT tenant_id
|
||||||
|
FROM ptmain.user_tenants
|
||||||
|
WHERE user_id = current_setting('app.user_id', TRUE)::INTEGER
|
||||||
|
));",
|
||||||
|
"DROP POLICY IF EXISTS user_tenant_access ON ptmain.user_tenants;",
|
||||||
|
@"
|
||||||
|
CREATE POLICY user_tenant_access ON ptmain.user_tenants
|
||||||
|
USING (user_id = current_setting('app.user_id', TRUE)::INTEGER);"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes one or more SQL commands within a transaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sqlCommands">The SQL commands to execute.</param>
|
||||||
|
private void ExecuteInTransaction(params string[] sqlCommands)
|
||||||
|
{
|
||||||
|
if (_db.State != ConnectionState.Open)
|
||||||
|
_db.Open();
|
||||||
|
|
||||||
|
using var transaction = _db.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var sql in sqlCommands)
|
||||||
|
{
|
||||||
|
_db.ExecuteSql(sql, transaction: transaction);
|
||||||
|
}
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new InvalidOperationException("Failed to execute SQL commands in transaction.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Database/RolesPermissionSystem/Setup.cs
Normal file
101
Database/RolesPermissionSystem/Setup.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
using System.Data;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Insight.Database;
|
||||||
|
|
||||||
|
namespace Database.Tenants
|
||||||
|
{
|
||||||
|
public class Setup
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _db;
|
||||||
|
|
||||||
|
public Setup(IDbConnection db)
|
||||||
|
{
|
||||||
|
_db = db ?? throw new ArgumentNullException(nameof(db));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the system tables in the specified schema within a transaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="schema">The schema name where the tables will be created.</param>
|
||||||
|
public async Task CreateSystem(string schema)
|
||||||
|
{
|
||||||
|
if (!IsValidSchemaName(schema))
|
||||||
|
throw new ArgumentException("Invalid schema name", nameof(schema));
|
||||||
|
|
||||||
|
using (var transaction = _db.BeginTransaction())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CreateRolesTable(schema, transaction).ConfigureAwait(false);
|
||||||
|
await CreatePermissionsTable(schema, transaction).ConfigureAwait(false);
|
||||||
|
await CreateRolePermissionsTable(schema, transaction).ConfigureAwait(false);
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
transaction.Rollback();
|
||||||
|
throw new InvalidOperationException("Failed to create system tables.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsValidSchemaName(string schema)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(schema) && Regex.IsMatch(schema, "^[a-zA-Z0-9_]+$");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteSqlAsync(string sql, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sql))
|
||||||
|
throw new ArgumentNullException(nameof(sql));
|
||||||
|
|
||||||
|
await _db.ExecuteAsync(sql, transaction: transaction).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreatePermissionTypesTable(string schema, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
var sql = $@"
|
||||||
|
CREATE TABLE IF NOT EXISTS {schema}.permission_types (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE
|
||||||
|
)";
|
||||||
|
await ExecuteSqlAsync(sql, transaction).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreatePermissionsTable(string schema, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
var sql = $@"
|
||||||
|
CREATE TABLE IF NOT EXISTS {schema}.permissions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
type_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (type_id) REFERENCES {schema}.permission_types(id)
|
||||||
|
)";
|
||||||
|
await ExecuteSqlAsync(sql, transaction).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateRolesTable(string schema, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
var sql = $@"
|
||||||
|
CREATE TABLE IF NOT EXISTS {schema}.roles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE
|
||||||
|
)";
|
||||||
|
await ExecuteSqlAsync(sql, transaction).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateRolePermissionsTable(string schema, IDbTransaction transaction)
|
||||||
|
{
|
||||||
|
var sql = $@"
|
||||||
|
CREATE TABLE IF NOT EXISTS {schema}.role_permissions (
|
||||||
|
role_id INTEGER NOT NULL,
|
||||||
|
permission_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (role_id, permission_id),
|
||||||
|
FOREIGN KEY (role_id) REFERENCES {schema}.roles(id),
|
||||||
|
FOREIGN KEY (permission_id) REFERENCES {schema}.permissions(id)
|
||||||
|
)";
|
||||||
|
await ExecuteSqlAsync(sql, transaction).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,11 @@ using System.Data;
|
||||||
|
|
||||||
namespace Database.Tenants
|
namespace Database.Tenants
|
||||||
{
|
{
|
||||||
internal class TenantData
|
internal class InitializeTenantData
|
||||||
{
|
{
|
||||||
private readonly IDbConnection _db;
|
private readonly IDbConnection _db;
|
||||||
|
|
||||||
public TenantData(IDbConnection db)
|
public InitializeTenantData(IDbConnection db)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"resources":{"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"}}}
|
{"resources":{"Scripts/Script-2.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"}}}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"resources":{"Scripts/Script-2.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"}}}
|
{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-19484872d85-cd2a4a40116e706","default-catalog":"ptdb01","default-schema":"ptmain"},"Scripts/Script-2.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"}}}
|
||||||
0
SqlManagement/Scripts/Script-1.sql
Normal file
0
SqlManagement/Scripts/Script-1.sql
Normal file
|
|
@ -8,6 +8,7 @@ CREATE SCHEMA ptmain;
|
||||||
GRANT USAGE, CREATE ON SCHEMA ptmain TO sathumper;
|
GRANT USAGE, CREATE ON SCHEMA ptmain TO sathumper;
|
||||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA ptmain TO sathumper;
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA ptmain TO sathumper;
|
||||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA ptmain TO sathumper;
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA ptmain TO sathumper;
|
||||||
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA ptmain
|
ALTER DEFAULT PRIVILEGES IN SCHEMA ptmain
|
||||||
GRANT ALL PRIVILEGES ON TABLES TO sathumper;
|
GRANT ALL PRIVILEGES ON TABLES TO sathumper;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
|
|
||||||
GRANT USAGE, CREATE ON SCHEMA swp TO sathumper;
|
|
||||||
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA swp
|
|
||||||
GRANT ALL PRIVILEGES ON TABLES TO sathumper;
|
|
||||||
|
|
||||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA swp TO sathumper;
|
|
||||||
|
|
||||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA swp TO sathumper;
|
|
||||||
|
|
||||||
|
|
||||||
select * from dev.app_configuration
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION dev.notify_config_change()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
IF TG_OP = 'DELETE' THEN
|
|
||||||
PERFORM pg_notify(
|
|
||||||
'config_changes',
|
|
||||||
json_build_object('operation', TG_OP, 'data', row_to_json(OLD))::text
|
|
||||||
);
|
|
||||||
RETURN OLD; -- Return OLD for DELETE operations
|
|
||||||
ELSE
|
|
||||||
PERFORM pg_notify(
|
|
||||||
'config_changes',
|
|
||||||
json_build_object('operation', TG_OP, 'data', row_to_json(NEW))::text
|
|
||||||
);
|
|
||||||
RETURN NEW; -- Return NEW for INSERT/UPDATE operations
|
|
||||||
END IF;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Trigger på configuration tabellen
|
|
||||||
CREATE TRIGGER config_change_trigger
|
|
||||||
AFTER INSERT OR UPDATE OR DELETE ON dev.app_configuration
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION dev.notify_config_change();
|
|
||||||
|
|
||||||
|
|
||||||
update dev.app_configuration
|
|
||||||
set "label" = "label" where id = 3
|
|
||||||
|
|
||||||
SELECT row_to_json(t)
|
|
||||||
FROM (SELECT 1 as id, 'test' as key) t;
|
|
||||||
|
|
||||||
SET myapp.tenant_id = '1';
|
|
||||||
|
|
||||||
SHOW myapp.tenant_id;
|
|
||||||
|
|
||||||
create TABLE dev.app_configuration1 (
|
|
||||||
tenant_id varchar(25),
|
|
||||||
config_key VARCHAR(255),
|
|
||||||
config_value TEXT,
|
|
||||||
PRIMARY KEY (tenant_id, config_key)
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE dev.app_configuration1 ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
CREATE drop POLICY tenant_policy
|
|
||||||
ON dev.app_configuration1
|
|
||||||
FOR SELECT
|
|
||||||
USING (
|
|
||||||
current_setting('myapp.tenant_id', true) IS NOT NULL AND
|
|
||||||
tenant_id = current_setting('myapp.tenant_id')::INT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE POLICY tenant_policy
|
|
||||||
ON dev.app_configuration1
|
|
||||||
FOR SELECT
|
|
||||||
USING (
|
|
||||||
tenant_id::text = current_user
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into dev.app_configuration1
|
|
||||||
(tenant_id, config_key, config_value) values('dev', 't2', 'best dat')
|
|
||||||
|
|
||||||
SET myapp.tenant_id = 0
|
|
||||||
|
|
||||||
ALTER USER din_bruger NOBYPASS RLS;
|
|
||||||
select * from dev.app_configuration1
|
|
||||||
|
|
||||||
SHOW row_security;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GRANT USAGE, CREATE ON SCHEMA dev TO sathumper;
|
|
||||||
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA dev
|
|
||||||
GRANT ALL PRIVILEGES ON TABLES TO sathumper;
|
|
||||||
|
|
||||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA dev TO sathumper;
|
|
||||||
|
|
||||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA dev TO sathumper;
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue