PlanTempusApp/Database/Core/DDL/SetupIdentitySystem.cs
Janus C. H. Knudsen 88812177a9 Migrate from User to Account domain concept
Renames core domain entities and services from "User" to "Account"

Refactors project-wide namespaces, classes, and database tables to use "Account" terminology
Updates related components, services, and database schema to reflect new domain naming
Standardizes naming conventions across authentication and organization setup features
2026-01-09 22:14:46 +01:00

140 lines
No EOL
5.3 KiB
C#

using Insight.Database;
using System.Data;
using PlanTempus.Core.Database.ConnectionFactory;
namespace PlanTempus.Database.Core.DDL
{
/// <summary>
/// This is by purpose not async await
/// It is intended that this is created with the correct Application User, which is why the schema name is omitted.
/// </summary>
public class SetupIdentitySystem : IDbConfigure<SetupIdentitySystem.Command>
{
public class Command
{
public required string Schema { get; init; }
}
Command _command;
private readonly IDbConnectionFactory _connectionFactory;
public SetupIdentitySystem(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
/// <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 void With(Command command, ConnectionStringParameters parameters = null)
{
_command = command;
using var conn = parameters is null ? _connectionFactory.Create() : _connectionFactory.Create(parameters);
using var transaction = conn.OpenWithTransaction();
try
{
CreateAccountsTable(conn);
CreateOrganizationsTable(conn);
CreateAccountOrganizationsTable(conn);
SetupRLS(conn);
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw new InvalidOperationException("Failed to SetupIdentitySystem. Transaction is rolled back", ex);
}
}
/// <summary>
/// Creates the accounts table
/// </summary>
void CreateAccountsTable(IDbConnection db)
{
var sql = @$"
CREATE TABLE IF NOT EXISTS {_command.Schema}.accounts (
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
);";
db.ExecuteSql(sql);
}
/// <summary>
/// Creates the organizations table
/// </summary>
void CreateOrganizationsTable(IDbConnection db)
{
var sql = @$"
CREATE TABLE IF NOT EXISTS {_command.Schema}.organizations (
id SERIAL PRIMARY KEY,
connection_string VARCHAR(500) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);";
db.ExecuteSql(sql);
}
/// <summary>
/// Creates the account_organizations table
/// </summary>
void CreateAccountOrganizationsTable(IDbConnection db)
{
var sql = @$"
CREATE TABLE IF NOT EXISTS {_command.Schema}.account_organizations (
account_id INTEGER NOT NULL REFERENCES {_command.Schema}.accounts(id),
organization_id INTEGER NOT NULL REFERENCES {_command.Schema}.organizations(id),
pin_code VARCHAR(10) NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (account_id, organization_id)
);";
db.ExecuteSql(sql);
}
/// <summary>
/// Sets up Row Level Security (RLS) for the organizations and account_organizations tables.
/// </summary>
void SetupRLS(IDbConnection db)
{
var sql = new[]
{
$"ALTER TABLE {_command.Schema}.organizations ENABLE ROW LEVEL SECURITY;",
$"ALTER TABLE {_command.Schema}.account_organizations ENABLE ROW LEVEL SECURITY;",
$"DROP POLICY IF EXISTS organization_access ON {_command.Schema}.organizations;",
@$"CREATE POLICY organization_access ON {_command.Schema}.organizations
USING (id IN (
SELECT organization_id
FROM {_command.Schema}.account_organizations
WHERE account_id = current_setting('app.account_id', TRUE)::INTEGER
)) WITH CHECK (true);",
$"DROP POLICY IF EXISTS account_organization_access ON {_command.Schema}.account_organizations;",
@$"CREATE POLICY account_organization_access ON {_command.Schema}.account_organizations
USING (account_id = current_setting('app.account_id', TRUE)::INTEGER) WITH CHECK (true);"
};
foreach (var statement in sql)
{
db.ExecuteSql(statement);
}
}
}
}