diff --git a/Application/Components/ApiViewComponentBase.cs b/Application/Components/ApiViewComponentBase.cs
new file mode 100644
index 0000000..0d1ab31
--- /dev/null
+++ b/Application/Components/ApiViewComponentBase.cs
@@ -0,0 +1,137 @@
+using Microsoft.ApplicationInsights;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace PlanTempus.Application.Components
+{
+ public abstract class ApiViewComponentBase : ViewComponent
+ {
+ private readonly HttpClient _httpClient;
+ private readonly TelemetryClient _telemetry;
+
+ protected ApiViewComponentBase(IHttpClientFactory httpClientFactory, TelemetryClient telemetry)
+ {
+ _httpClient = httpClientFactory.CreateClient("ApiClient");
+ _telemetry = telemetry;
+ }
+
+ ///
+ /// Fetches data from API as JObject
+ ///
+ /// API endpoint path (e.g. "/api/product/get/1")
+ /// JObject with API result
+ protected async Task GetJObjectFromApiAsync(string apiEndpoint)
+ {
+ try
+ {
+ _telemetry.TrackEvent("ApiCall", new Dictionary
+ {
+ ["Endpoint"] = apiEndpoint,
+ ["Source"] = "GetJObjectFromApiAsync"
+ });
+
+ // Use HttpClient to get JSON string
+ var response = await _httpClient.GetAsync(apiEndpoint);
+ response.EnsureSuccessStatusCode();
+
+ var jsonString = await response.Content.ReadAsStringAsync();
+ return JObject.Parse(jsonString);
+ }
+ catch (Exception ex)
+ {
+ _telemetry.TrackException(ex, new Dictionary
+ {
+ ["Endpoint"] = apiEndpoint,
+ ["Method"] = "GetJObjectFromApiAsync"
+ });
+ return null;
+ }
+ }
+
+ ///
+ /// Fetches data from API as JArray
+ ///
+ /// API endpoint path
+ /// JArray with API result
+ protected async Task GetJArrayFromApiAsync(string apiEndpoint)
+ {
+ try
+ {
+ _telemetry.TrackEvent("ApiCall", new Dictionary
+ {
+ ["Endpoint"] = apiEndpoint,
+ ["Source"] = "GetJArrayFromApiAsync"
+ });
+
+ // Use HttpClient to get JSON string
+ var response = await _httpClient.GetAsync(apiEndpoint);
+ response.EnsureSuccessStatusCode();
+
+ var jsonString = await response.Content.ReadAsStringAsync();
+ return JArray.Parse(jsonString);
+ }
+ catch (Exception ex)
+ {
+ _telemetry.TrackException(ex, new Dictionary
+ {
+ ["Endpoint"] = apiEndpoint,
+ ["Method"] = "GetJArrayFromApiAsync"
+ });
+ return null;
+ }
+ }
+
+ ///
+ /// Sends POST request to API and receives JObject response
+ ///
+ /// API endpoint path
+ /// Data to be sent (can be JObject or other type)
+ /// JObject with response data
+ protected async Task PostToApiAsync(string apiEndpoint, object data)
+ {
+ try
+ {
+ _telemetry.TrackEvent("ApiPost", new Dictionary
+ {
+ ["Endpoint"] = apiEndpoint
+ });
+
+ // Convert data to JSON string
+ var content = new StringContent(
+ JsonConvert.SerializeObject(data),
+ System.Text.Encoding.UTF8,
+ "application/json");
+
+ var response = await _httpClient.PostAsync(apiEndpoint, content);
+ response.EnsureSuccessStatusCode();
+
+ var jsonString = await response.Content.ReadAsStringAsync();
+ return JObject.Parse(jsonString);
+ }
+ catch (Exception ex)
+ {
+ _telemetry.TrackException(ex, new Dictionary
+ {
+ ["Endpoint"] = apiEndpoint,
+ ["Method"] = "PostToApiAsync"
+ });
+ return null;
+ }
+ }
+
+ ///
+ /// Handles errors in a consistent way
+ ///
+ protected IViewComponentResult HandleError(string message = "An error occurred.")
+ {
+ _telemetry.TrackEvent("ComponentError", new Dictionary
+ {
+ ["Message"] = message,
+ ["Component"] = this.GetType().Name
+ });
+
+ return Content(message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Components/OrganizationViewComponent.cs b/Application/Components/OrganizationViewComponent.cs
new file mode 100644
index 0000000..bfe7f9d
--- /dev/null
+++ b/Application/Components/OrganizationViewComponent.cs
@@ -0,0 +1,43 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.ApplicationInsights;
+
+namespace PlanTempus.Application.Components
+{
+ public class OrganizationViewComponent : ApiViewComponentBase
+ {
+ private readonly TelemetryClient _telemetry;
+
+ public OrganizationViewComponent(
+ IHttpClientFactory httpClientFactory,
+ TelemetryClient telemetry)
+ : base(httpClientFactory, telemetry)
+ {
+ _telemetry = telemetry;
+ }
+
+ public async Task InvokeAsync(int organizationId, bool showDetailedView = true)
+ {
+ _telemetry.TrackEvent($"{GetType().Name}Invoked", new Dictionary
+ {
+ ["OrganizationId"] = organizationId.ToString(),
+ ["ShowDetailedView"] = showDetailedView.ToString()
+ });
+
+ var organization = await GetJObjectFromApiAsync($"/api/organization/get/{organizationId}");
+
+ if (organization == null)
+ return HandleError("Organization not found");
+
+ ViewBag.ShowDetailedView = showDetailedView;
+
+
+ _telemetry.TrackEvent("Viewed", new Dictionary
+ {
+ ["OrganizationId"] = organizationId.ToString(),
+ ["Name"] = organization["name"]?.ToString()
+ });
+
+ return View(organization);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Core/Logging/SeqLogger.cs b/Core/Logging/SeqLogger.cs
index 1f1a9fb..709f97a 100644
--- a/Core/Logging/SeqLogger.cs
+++ b/Core/Logging/SeqLogger.cs
@@ -3,238 +3,250 @@ using System.Text;
namespace PlanTempus.Core.Logging
{
- public class SeqLogger
- {
- private readonly SeqHttpClient _httpClient;
- private readonly string _environmentName;
- private readonly string _machineName;
- private readonly SeqConfiguration _configuration;
+ public class SeqLogger
+ {
+ private readonly SeqHttpClient _httpClient;
+ private readonly string _environmentName;
+ private readonly string _machineName;
+ private readonly SeqConfiguration _configuration;
- public SeqLogger(SeqHttpClient httpClient, SeqConfiguration configuration)
- {
- _httpClient = httpClient;
- _configuration = configuration;
- }
+ public SeqLogger(SeqHttpClient httpClient, SeqConfiguration configuration)
+ {
+ _httpClient = httpClient;
+ _configuration = configuration;
+ }
- public async Task LogAsync(TraceTelemetry trace, CancellationToken cancellationToken = default)
- {
- var seqEvent = new Dictionary
- {
- { "@t", trace.Timestamp.UtcDateTime.ToString("o") },
- { "@mt", trace.Message },
- { "@l", MapSeverityToLevel(trace.SeverityLevel) },
- { "Environment", _environmentName },
- { "MachineName", _machineName }
- };
+ public async Task LogAsync(TraceTelemetry trace, CancellationToken cancellationToken = default)
+ {
+ var seqEvent = new Dictionary
+ {
+ { "@t", trace.Timestamp.UtcDateTime.ToString("o") },
+ { "@mt", trace.Message },
+ { "@l", MapSeverityToLevel(trace.SeverityLevel) },
+ { "Environment", _environmentName },
+ { "MachineName", _machineName }
+ };
- foreach (var prop in trace.Properties)
- seqEvent.Add($"prop_{prop.Key}", prop.Value);
+ foreach (var prop in trace.Properties)
+ seqEvent.Add($"prop_{prop.Key}", prop.Value);
- foreach (var prop in trace.Context.GlobalProperties)
- seqEvent.Add($"global_{prop.Key}", prop.Value);
+ foreach (var prop in trace.Context.GlobalProperties)
+ seqEvent.Add($"global_{prop.Key}", prop.Value);
- await SendToSeqAsync(seqEvent, cancellationToken);
- }
+ await SendToSeqAsync(seqEvent, cancellationToken);
+ }
- public async Task LogAsync(EventTelemetry evt, CancellationToken cancellationToken = default)
- {
- var seqEvent = new Dictionary
- {
- { "@t", evt.Timestamp.UtcDateTime.ToString("o") },
- { "@mt", evt.Name },
- { "@l", "Information" },
- { "Environment", _environmentName },
- { "MachineName", _machineName }
- };
+ public async Task LogAsync(EventTelemetry evt, CancellationToken cancellationToken = default)
+ {
+ var seqEvent = new Dictionary
+ {
+ { "@t", evt.Timestamp.UtcDateTime.ToString("o") },
+ { "@mt", evt.Name },
+ { "@l", "Information" },
+ { "Environment", _environmentName },
+ { "MachineName", _machineName }
+ };
- foreach (var prop in evt.Properties)
- seqEvent.Add($"prop_{prop.Key}", prop.Value);
+ foreach (var prop in evt.Properties)
+ seqEvent.Add($"prop_{prop.Key}", prop.Value);
- foreach (var prop in evt.Context.GlobalProperties)
- seqEvent.Add($"global_{prop.Key}", prop.Value);
+ foreach (var prop in evt.Context.GlobalProperties)
+ seqEvent.Add($"global_{prop.Key}", prop.Value);
- foreach (var metric in evt.Metrics)
- seqEvent.Add($"metric_{metric.Key}", metric.Value);
+ foreach (var metric in evt.Metrics)
+ seqEvent.Add($"metric_{metric.Key}", metric.Value);
- await SendToSeqAsync(seqEvent, cancellationToken);
- }
+ await SendToSeqAsync(seqEvent, cancellationToken);
+ }
- public async Task LogAsync(ExceptionTelemetry ex, CancellationToken cancellationToken = default)
- {
- var seqEvent = new Dictionary
- {
- { "@t", ex.Timestamp.UtcDateTime.ToString("o") },
- { "@mt", ex.Exception.Message },
- { "@l", "Error" },
- { "@x", FormatExceptionForSeq(ex.Exception) },
- { "Environment", _environmentName },
- { "MachineName", _machineName },
- { "ExceptionType", ex.Exception.GetType().Name },
- };
+ public async Task LogAsync(ExceptionTelemetry ex, CancellationToken cancellationToken = default)
+ {
+ var seqEvent = new Dictionary
+ {
+ { "@t", ex.Timestamp.UtcDateTime.ToString("o") },
+ { "@mt", ex.Exception.Message },
+ { "@l", "Error" },
+ { "@x", FormatExceptionForSeq(ex.Exception) },
+ { "Environment", _environmentName },
+ { "MachineName", _machineName },
+ { "ExceptionType", ex.Exception.GetType().Name },
+ };
- foreach (var prop in ex.Properties)
- seqEvent.Add($"prop_{prop.Key}", prop.Value);
+ foreach (var prop in ex.Properties)
+ seqEvent.Add($"prop_{prop.Key}", prop.Value);
- foreach (var prop in ex.Context.GlobalProperties)
- seqEvent.Add($"global_{prop.Key}", prop.Value);
+ foreach (var prop in ex.Context.GlobalProperties)
+ seqEvent.Add($"global_{prop.Key}", prop.Value);
- await SendToSeqAsync(seqEvent, cancellationToken);
- }
+ await SendToSeqAsync(seqEvent, cancellationToken);
+ }
- public async Task LogAsync(DependencyTelemetry dep, CancellationToken cancellationToken = default)
- {
- var seqEvent = new Dictionary
- {
- { "@t", dep.Timestamp.UtcDateTime.ToString("o") },
- { "@mt", $"Dependency: {dep.Name}" },
- { "@l", dep.Success??true ? "Information" : "Error" },
- { "Environment", _environmentName },
- { "MachineName", _machineName },
- { "DependencyType", dep.Type },
- { "Target", dep.Target },
- { "Duration", dep.Duration.TotalMilliseconds }
- };
+ public async Task LogAsync(DependencyTelemetry dep, CancellationToken cancellationToken = default)
+ {
+ var seqEvent = new Dictionary
+ {
+ { "@t", dep.Timestamp.UtcDateTime.ToString("o") },
+ { "@mt", $"Dependency: {dep.Name}" },
+ { "@l", dep.Success ?? true ? "Information" : "Error" },
+ { "Environment", _environmentName },
+ { "MachineName", _machineName },
+ { "DependencyType", dep.Type },
+ { "Target", dep.Target },
+ { "Duration", dep.Duration.TotalMilliseconds }
+ };
- foreach (var prop in dep.Properties)
- seqEvent.Add($"prop_{prop.Key}", prop.Value);
+ foreach (var prop in dep.Properties)
+ seqEvent.Add($"prop_{prop.Key}", prop.Value);
- foreach (var prop in dep.Context.GlobalProperties)
- seqEvent.Add($"global_{prop.Key}", prop.Value);
+ foreach (var prop in dep.Context.GlobalProperties)
+ seqEvent.Add($"global_{prop.Key}", prop.Value);
- await SendToSeqAsync(seqEvent, cancellationToken);
- }
+ await SendToSeqAsync(seqEvent, cancellationToken);
+ }
- public async Task LogAsync(RequestTelemetry req, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException();
- }
- public async Task LogAsync(Microsoft.ApplicationInsights.Extensibility.IOperationHolder operationHolder, CancellationToken cancellationToken = default)
- {
- var req = operationHolder.Telemetry;
+ public async Task LogAsync(RequestTelemetry req, CancellationToken cancellationToken = default)
+ {
+ await Task.CompletedTask;
+ throw new NotImplementedException();
+ }
- //https://docs.datalust.co/v2025.1/docs/posting-raw-events
- var seqEvent = new Dictionary
- {
+ public async Task LogAsync(
+ Microsoft.ApplicationInsights.Extensibility.IOperationHolder operationHolder,
+ CancellationToken cancellationToken = default)
+ {
+ var req = operationHolder.Telemetry;
- { "@t", req.Timestamp.UtcDateTime.ToString("o") },
- { "@mt",req.Name },
- { "@l", req.Success??true ? "Information" : "Error" },
- { "@sp", req.Id }, //Span id Unique identifier of a span Yes, if the event is a span
- { "@tr", req.Context.Operation.Id}, //Trace id An identifier that groups all spans and logs that are in the same trace Yes, if the event is a span
- { "@sk","Server" }, //Span kind Describes the relationship of the span to others in the trace: Client, Server, Internal, Producer, or Consumer
- { "@st", req.Timestamp.UtcDateTime.Subtract(req.Duration).ToString("o") }, //Start The start ISO 8601 timestamp of this span Yes, if the event is a span
+ //https://docs.datalust.co/v2025.1/docs/posting-raw-events
+ var seqEvent = new Dictionary
+ {
+ { "@t", req.Timestamp.UtcDateTime.ToString("o") },
+ { "@mt", req.Name },
+ { "@l", req.Success ?? true ? "Information" : "Error" },
+ { "@sp", req.Id }, //Span id Unique identifier of a span Yes, if the event is a span
+ {
+ "@tr", req.Context.Operation.Id
+ }, //Trace id An identifier that groups all spans and logs that are in the same trace Yes, if the event is a span
+ {
+ "@sk", "Server"
+ }, //Span kind Describes the relationship of the span to others in the trace: Client, Server, Internal, Producer, or Consumer
+ {
+ "@st", req.Timestamp.UtcDateTime.Subtract(req.Duration).ToString("o")
+ }, //Start The start ISO 8601 timestamp of this span Yes, if the event is a span
{ "SourceContext", typeof(T).FullName },
- { "Url", req.Url },
- { "RequestId", req.Id },
- { "ItemTypeFlag", req.ItemTypeFlag.ToString() }
- };
+ { "Url", req.Url },
+ { "RequestId", req.Id },
+ { "ItemTypeFlag", req.ItemTypeFlag.ToString() }
+ };
- if (!string.IsNullOrEmpty(req.ResponseCode))
- {
- if (int.TryParse(req.ResponseCode, out int statusCode))
- {
- if (Enum.IsDefined(typeof(System.Net.HttpStatusCode), statusCode))
- seqEvent["StatusCode"] = $"{statusCode} {(System.Net.HttpStatusCode)statusCode}";
- else
- seqEvent["StatusCode"] = $"{statusCode} Unknown";
- }
- }
- if (!string.IsNullOrEmpty(req.Context.Operation.ParentId))
- seqEvent["@ps"] = req.Context.Operation.ParentId;
+ if (!string.IsNullOrEmpty(req.ResponseCode))
+ {
+ if (int.TryParse(req.ResponseCode, out int statusCode))
+ {
+ if (Enum.IsDefined(typeof(System.Net.HttpStatusCode), statusCode))
+ seqEvent["StatusCode"] = $"{statusCode} {(System.Net.HttpStatusCode)statusCode}";
+ else
+ seqEvent["StatusCode"] = $"{statusCode} Unknown";
+ }
+ }
- if (req.Properties.TryGetValue("httpMethod", out string method))
- {
- seqEvent["RequestMethod"] = method;
- seqEvent["@mt"] = $"{req.Properties["httpMethod"]} {req.Name}";
- req.Properties.Remove("httpMethod");
- }
+ if (!string.IsNullOrEmpty(req.Context.Operation.ParentId))
+ seqEvent["@ps"] = req.Context.Operation.ParentId;
- foreach (var prop in req.Properties)
- seqEvent.Add($"prop_{prop.Key}", prop.Value);
+ if (req.Properties.TryGetValue("httpMethod", out string method))
+ {
+ seqEvent["RequestMethod"] = method;
+ seqEvent["@mt"] = $"{req.Properties["httpMethod"]} {req.Name}";
+ req.Properties.Remove("httpMethod");
+ }
- foreach (var prop in req.Context.GlobalProperties)
- seqEvent.Add($"{prop.Key}", prop.Value);
+ foreach (var prop in req.Properties)
+ seqEvent.Add($"prop_{prop.Key}", prop.Value);
- await SendToSeqAsync(seqEvent, cancellationToken);
- }
- private async Task SendToSeqAsync(Dictionary seqEvent, CancellationToken cancellationToken)
- {
- var content = new StringContent(
- Newtonsoft.Json.JsonConvert.SerializeObject(seqEvent),
- Encoding.UTF8,
- "application/vnd.serilog.clef");
+ foreach (var prop in req.Context.GlobalProperties)
+ seqEvent.Add($"{prop.Key}", prop.Value);
- var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/ingest/clef")
- {
- Content = content
- };
+ await SendToSeqAsync(seqEvent, cancellationToken);
+ }
- var result = await _httpClient.SendAsync(requestMessage, cancellationToken);
+ private async Task SendToSeqAsync(Dictionary seqEvent, CancellationToken cancellationToken)
+ {
+ var content = new StringContent(
+ Newtonsoft.Json.JsonConvert.SerializeObject(seqEvent),
+ Encoding.UTF8,
+ "application/vnd.serilog.clef");
- result.EnsureSuccessStatusCode();
- }
+ var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/ingest/clef")
+ {
+ Content = content
+ };
- private static string MapSeverityToLevel(SeverityLevel? severity)
- {
- return severity switch
- {
- SeverityLevel.Verbose => "Verbose",
- SeverityLevel.Information => "Information",
- SeverityLevel.Warning => "Warning",
- SeverityLevel.Error => "Error",
- SeverityLevel.Critical => "Fatal",
- _ => "Information"
- };
- }
- private static string FormatExceptionForSeq(Exception ex)
- {
- var sb = new StringBuilder();
- var exceptionCount = 0;
+ var result = await _httpClient.SendAsync(requestMessage, cancellationToken);
- void FormatSingleException(Exception currentEx, int depth)
- {
- if (depth > 0) sb.AppendLine("\n--- Inner Exception ---");
+ result.EnsureSuccessStatusCode();
+ }
- sb.AppendLine($"Exception Type: {currentEx.GetType().FullName}");
- sb.AppendLine($"Message: {currentEx.Message}");
- sb.AppendLine($"Source: {currentEx.Source}");
- sb.AppendLine($"HResult: 0x{currentEx.HResult:X8}");
- sb.AppendLine("Stack Trace:");
- sb.AppendLine(currentEx.StackTrace?.Trim());
+ private static string MapSeverityToLevel(SeverityLevel? severity)
+ {
+ return severity switch
+ {
+ SeverityLevel.Verbose => "Verbose",
+ SeverityLevel.Information => "Information",
+ SeverityLevel.Warning => "Warning",
+ SeverityLevel.Error => "Error",
+ SeverityLevel.Critical => "Fatal",
+ _ => "Information"
+ };
+ }
- if (currentEx.Data.Count > 0)
- {
- sb.AppendLine("Additional Data:");
- foreach (var key in currentEx.Data.Keys)
- {
- sb.AppendLine($" {key}: {currentEx.Data[key]}");
- }
- }
- }
+ private static string FormatExceptionForSeq(Exception ex)
+ {
+ var sb = new StringBuilder();
+ var exceptionCount = 0;
- void RecurseExceptions(Exception currentEx, int depth = 0)
- {
- if (currentEx is AggregateException aggEx)
- {
- foreach (var inner in aggEx.InnerExceptions)
- {
- RecurseExceptions(inner, depth);
- depth++;
- }
- }
- else if (currentEx.InnerException != null)
- {
- RecurseExceptions(currentEx.InnerException, depth + 1);
- }
+ void FormatSingleException(Exception currentEx, int depth)
+ {
+ if (depth > 0) sb.AppendLine("\n--- Inner Exception ---");
- FormatSingleException(currentEx, depth);
- exceptionCount++;
- }
+ sb.AppendLine($"Exception Type: {currentEx.GetType().FullName}");
+ sb.AppendLine($"Message: {currentEx.Message}");
+ sb.AppendLine($"Source: {currentEx.Source}");
+ sb.AppendLine($"HResult: 0x{currentEx.HResult:X8}");
+ sb.AppendLine("Stack Trace:");
+ sb.AppendLine(currentEx.StackTrace?.Trim());
- RecurseExceptions(ex);
- sb.Insert(0, $"EXCEPTION CHAIN ({exceptionCount} exceptions):\n");
- return sb.ToString();
- }
- }
-}
+ if (currentEx.Data.Count > 0)
+ {
+ sb.AppendLine("Additional Data:");
+ foreach (var key in currentEx.Data.Keys)
+ {
+ sb.AppendLine($" {key}: {currentEx.Data[key]}");
+ }
+ }
+ }
+
+ void RecurseExceptions(Exception currentEx, int depth = 0)
+ {
+ if (currentEx is AggregateException aggEx)
+ {
+ foreach (var inner in aggEx.InnerExceptions)
+ {
+ RecurseExceptions(inner, depth);
+ depth++;
+ }
+ }
+ else if (currentEx.InnerException != null)
+ {
+ RecurseExceptions(currentEx.InnerException, depth + 1);
+ }
+
+ FormatSingleException(currentEx, depth);
+ exceptionCount++;
+ }
+
+ RecurseExceptions(ex);
+ sb.Insert(0, $"EXCEPTION CHAIN ({exceptionCount} exceptions):\n");
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Core/MultiKeyEncryption/MasterKey.cs b/Core/MultiKeyEncryption/MasterKey.cs
index 3b16729..c86fa93 100644
--- a/Core/MultiKeyEncryption/MasterKey.cs
+++ b/Core/MultiKeyEncryption/MasterKey.cs
@@ -4,6 +4,7 @@
{
public async Task RotateMasterKey(int tenantId, string oldMasterKey, string newMasterKey)
{
+ await Task.CompletedTask;
// Hent alle bruger-keys for tenant
//var users = await GetTenantUsers(tenantId);
diff --git a/Core/SecureTokenizer.cs b/Core/SecureTokenizer.cs
index bffe593..8059951 100644
--- a/Core/SecureTokenizer.cs
+++ b/Core/SecureTokenizer.cs
@@ -1,6 +1,5 @@
namespace PlanTempus.Core
{
-
public class SecureTokenizer : ISecureTokenizer
{
private const int _saltSize = 16; // 128 bit
diff --git a/Core/Sql/ConnectionFactory/IDbConnectionFactory.cs b/Core/Sql/ConnectionFactory/IDbConnectionFactory.cs
index 06b0dd2..2957a7d 100644
--- a/Core/Sql/ConnectionFactory/IDbConnectionFactory.cs
+++ b/Core/Sql/ConnectionFactory/IDbConnectionFactory.cs
@@ -1,7 +1,5 @@
namespace PlanTempus.Core.Sql.ConnectionFactory
{
- public record ConnectionStringParameters(string user, string pwd);
-
public interface IDbConnectionFactory
{
System.Data.IDbConnection Create();
diff --git a/Core/Sql/ConnectionFactory/PostgresConnectionFactory.cs b/Core/Sql/ConnectionFactory/PostgresConnectionFactory.cs
index d40801e..9f2906d 100644
--- a/Core/Sql/ConnectionFactory/PostgresConnectionFactory.cs
+++ b/Core/Sql/ConnectionFactory/PostgresConnectionFactory.cs
@@ -3,6 +3,7 @@ using System.Data;
namespace PlanTempus.Core.Sql.ConnectionFactory
{
+ public record ConnectionStringParameters(string User, string Pwd);
public class PostgresConnectionFactory : IDbConnectionFactory, IAsyncDisposable
{
@@ -34,8 +35,8 @@ namespace PlanTempus.Core.Sql.ConnectionFactory
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(
_baseDataSource.ConnectionString)
{
- Username = param.user,
- Password = param.pwd
+ Username = param.User,
+ Password = param.Pwd
};
var tempDataSourceBuilder = new NpgsqlDataSourceBuilder(
diff --git a/Core/Sql/DatabaseScope.cs b/Core/Sql/DatabaseScope.cs
new file mode 100644
index 0000000..34cdf0c
--- /dev/null
+++ b/Core/Sql/DatabaseScope.cs
@@ -0,0 +1,35 @@
+using System.Data;
+using Microsoft.ApplicationInsights.DataContracts;
+using Microsoft.ApplicationInsights.Extensibility;
+
+namespace PlanTempus.Core.Sql;
+
+public class DatabaseScope : IDisposable
+{
+ private readonly IOperationHolder _operation;
+
+ public DatabaseScope(IDbConnection connection, IOperationHolder operation)
+ {
+ Connection = connection;
+ _operation = operation;
+ }
+
+ public IDbConnection Connection { get; }
+
+ public void Dispose()
+ {
+ _operation.Dispose();
+ Connection.Dispose();
+ }
+
+ public void Success()
+ {
+ _operation.Telemetry.Success = true;
+ }
+
+ public void Error(Exception ex)
+ {
+ _operation.Telemetry.Success = false;
+ _operation.Telemetry.Properties["Error"] = ex.Message;
+ }
+}
\ No newline at end of file
diff --git a/Core/Sql/IDatabaseOperations.cs b/Core/Sql/IDatabaseOperations.cs
new file mode 100644
index 0000000..8d4031d
--- /dev/null
+++ b/Core/Sql/IDatabaseOperations.cs
@@ -0,0 +1,10 @@
+using System.Data;
+
+namespace PlanTempus.Core.Sql;
+
+public interface IDatabaseOperations
+{
+ DatabaseScope CreateScope(string operationName);
+ Task ExecuteAsync(Func> operation, string operationName);
+ Task ExecuteAsync(Func operation, string operationName);
+}
\ No newline at end of file
diff --git a/Core/Sql/SqlOperations.cs b/Core/Sql/SqlOperations.cs
index 09bfc9b..aa31522 100644
--- a/Core/Sql/SqlOperations.cs
+++ b/Core/Sql/SqlOperations.cs
@@ -1,100 +1,59 @@
-using Microsoft.ApplicationInsights;
+using System.Data;
+using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
-using Microsoft.ApplicationInsights.Extensibility;
using PlanTempus.Core.Sql.ConnectionFactory;
-using System.Data;
-namespace PlanTempus.Core.Sql
+namespace PlanTempus.Core.Sql;
+
+public class SqlOperations : IDatabaseOperations
{
- public class DatabaseScope : IDisposable
- {
- private readonly IDbConnection _connection;
- private readonly IOperationHolder _operation;
+ private readonly IDbConnectionFactory _connectionFactory;
+ private readonly TelemetryClient _telemetryClient;
- public DatabaseScope(IDbConnection connection, IOperationHolder operation)
- {
- _connection = connection;
- _operation = operation;
- }
+ public SqlOperations(IDbConnectionFactory connectionFactory, TelemetryClient telemetryClient)
+ {
+ _connectionFactory = connectionFactory;
+ _telemetryClient = telemetryClient;
+ }
- public IDbConnection Connection => _connection;
+ public DatabaseScope CreateScope(string operationName)
+ {
+ var connection = _connectionFactory.Create();
+ var operation = _telemetryClient.StartOperation(operationName);
+ operation.Telemetry.Type = "SQL";
+ operation.Telemetry.Target = "PostgreSQL";
- public void Success()
- {
- _operation.Telemetry.Success = true;
- }
+ return new DatabaseScope(connection, operation);
+ }
- public void Error(Exception ex)
- {
- _operation.Telemetry.Success = false;
- _operation.Telemetry.Properties["Error"] = ex.Message;
- }
+ public async Task ExecuteAsync(Func> operation, string operationName)
+ {
+ using var scope = CreateScope(operationName);
+ try
+ {
+ var result = await operation(scope.Connection);
+ scope.Success();
+ return result;
+ }
+ catch (Exception ex)
+ {
+ scope.Error(ex);
+ throw;
+ }
+ }
- public void Dispose()
- {
- _operation.Dispose();
- _connection.Dispose();
- }
- }
-
- public interface IDatabaseOperations
- {
- DatabaseScope CreateScope(string operationName);
- Task ExecuteAsync(Func> operation, string operationName);
- Task ExecuteAsync(Func operation, string operationName);
- }
-
- public class SqlOperations : IDatabaseOperations
- {
- private readonly IDbConnectionFactory _connectionFactory;
- private readonly TelemetryClient _telemetryClient;
-
- public SqlOperations(IDbConnectionFactory connectionFactory, TelemetryClient telemetryClient)
- {
- _connectionFactory = connectionFactory;
- _telemetryClient = telemetryClient;
- }
-
- public DatabaseScope CreateScope(string operationName)
- {
- var connection = _connectionFactory.Create();
- var operation = _telemetryClient.StartOperation(operationName);
- operation.Telemetry.Type = "SQL";
- operation.Telemetry.Target = "PostgreSQL";
-
- return new DatabaseScope(connection, operation);
- }
-
- public async Task ExecuteAsync(Func> operation, string operationName)
- {
- using var scope = CreateScope(operationName);
- try
- {
- var result = await operation(scope.Connection);
- scope.Success();
- return result;
- }
- catch (Exception ex)
- {
- scope.Error(ex);
- throw;
- }
- }
-
- public async Task ExecuteAsync(Func operation, string operationName)
- {
- using var scope = CreateScope(operationName);
- try
- {
- await operation(scope.Connection);
- scope.Success();
- }
- catch (Exception ex)
- {
- scope.Error(ex);
- throw;
- }
- }
-
- }
-}
+ public async Task ExecuteAsync(Func operation, string operationName)
+ {
+ using var scope = CreateScope(operationName);
+ try
+ {
+ await operation(scope.Connection);
+ scope.Success();
+ }
+ catch (Exception ex)
+ {
+ scope.Error(ex);
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Database/ConfigurationManagementSystem/SetupConfiguration.cs b/Database/ConfigurationManagementSystem/SetupConfiguration.cs
index 4e4450d..4093a53 100644
--- a/Database/ConfigurationManagementSystem/SetupConfiguration.cs
+++ b/Database/ConfigurationManagementSystem/SetupConfiguration.cs
@@ -19,6 +19,7 @@ public class SetupConfiguration : IDbConfigure
{
using var conn = parameters is null ? _connectionFactory.Create() : _connectionFactory.Create(parameters);
using var transaction = conn.OpenWithTransaction();
+
try
{
CreateConfigurationTable(conn);
diff --git a/Database/Core/DCL/SetupDbAdmin.cs b/Database/Core/DCL/SetupDbAdmin.cs
index 42746ed..1d5a948 100644
--- a/Database/Core/DCL/SetupDbAdmin.cs
+++ b/Database/Core/DCL/SetupDbAdmin.cs
@@ -2,7 +2,6 @@
using Insight.Database;
using PlanTempus.Core.Sql.ConnectionFactory;
using PlanTempus.Database.Common;
-using PlanTempus.Database.Core;
namespace PlanTempus.Database.Core.DCL
{
diff --git a/PlanTempus.Components/Organizations/Create/CreateOrganizationCommand.cs b/PlanTempus.Components/Organizations/Create/CreateOrganizationCommand.cs
index bac9e3f..2b53493 100644
--- a/PlanTempus.Components/Organizations/Create/CreateOrganizationCommand.cs
+++ b/PlanTempus.Components/Organizations/Create/CreateOrganizationCommand.cs
@@ -3,7 +3,7 @@
public class CreateOrganizationCommand
{
public string Name { get; set; }
- public string Description { get; set; }
+ public string ConnectionString { get; set; }
public Guid CreatedById { get; set; }
}
}
diff --git a/PlanTempus.Components/Organizations/Create/CreateOrganizationHandler.cs b/PlanTempus.Components/Organizations/Create/CreateOrganizationHandler.cs
index 7b80156..51436ea 100644
--- a/PlanTempus.Components/Organizations/Create/CreateOrganizationHandler.cs
+++ b/PlanTempus.Components/Organizations/Create/CreateOrganizationHandler.cs
@@ -12,37 +12,27 @@ namespace PlanTempus.Components.Organizations.Create
_databaseOperations = databaseOperations;
}
- public async Task Handle(CreateOrganizationCommand command)
+ public async Task Handle(CreateOrganizationCommand command)
{
using var db = _databaseOperations.CreateScope(nameof(CreateOrganizationHandler));
+
try
{
- var organizationId = Guid.NewGuid();
- var now = DateTime.UtcNow;
-
- var sql = @"
- INSERT INTO organizations (id, name, description, created_by_id)
- VALUES (@Id, @Name, @Description, @CreatedById)";
+ var sql = @"
+ INSERT INTO organizations (connection_string, created_by)
+ VALUES (@ConnectionString, @CreatedBy)
+ RETURNING id, created_at";
- await db.Connection.ExecuteSqlAsync(sql, new
+ var data = await db.Connection.QuerySqlAsync(sql, new
{
- Id = organizationId,
- command.Name,
- command.Description,
- CreatedById = command.CreatedById,
- CreatedAt = now,
- UpdatedAt = now
+ ConnectionString = command.ConnectionString,
+ CreatedBy = command.CreatedById
});
db.Success();
- return new CreateOrganizationResponse
- {
- Id = organizationId,
- Name = command.Name,
- CreatedAt = now
- };
+ return data.First();
}
catch (Exception ex)
{
diff --git a/PlanTempus.Components/Organizations/Create/CreateOrganizationResponse.cs b/PlanTempus.Components/Organizations/Create/CreateOrganizationResponse.cs
deleted file mode 100644
index c181f9d..0000000
--- a/PlanTempus.Components/Organizations/Create/CreateOrganizationResponse.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace PlanTempus.Components.Organizations.Create
-{
- public class CreateOrganizationResponse
- {
- public Guid Id { get; set; }
- public string Name { get; set; }
- public DateTime CreatedAt { get; set; }
- }
-}
diff --git a/PlanTempus.Components/Organizations/Create/CreateOrganizationResult.cs b/PlanTempus.Components/Organizations/Create/CreateOrganizationResult.cs
new file mode 100644
index 0000000..27db38c
--- /dev/null
+++ b/PlanTempus.Components/Organizations/Create/CreateOrganizationResult.cs
@@ -0,0 +1,8 @@
+namespace PlanTempus.Components.Organizations.Create
+{
+ public class CreateOrganizationResult
+ {
+ public int Id { get; set; }
+ public DateTime CreatedAt { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/PlanTempus.Components/Users/Create/CreateUserController.cs b/PlanTempus.Components/Users/Create/CreateUserController.cs
index c4cd2f5..dc6eebc 100644
--- a/PlanTempus.Components/Users/Create/CreateUserController.cs
+++ b/PlanTempus.Components/Users/Create/CreateUserController.cs
@@ -7,7 +7,7 @@ namespace PlanTempus.Components.Users.Create
public class CreateUserController(CreateUserHandler handler, CreateUserValidator validator) : ControllerBase
{
[HttpPost]
- public async Task> Create([FromBody] CreateUserCommand command)
+ public async Task> Create([FromBody] CreateUserCommand command)
{
try
{
diff --git a/PlanTempus.Components/Users/Create/CreateUserHandler.cs b/PlanTempus.Components/Users/Create/CreateUserHandler.cs
index 259053d..f7d8694 100644
--- a/PlanTempus.Components/Users/Create/CreateUserHandler.cs
+++ b/PlanTempus.Components/Users/Create/CreateUserHandler.cs
@@ -5,60 +5,41 @@ using PlanTempus.Core.Sql;
namespace PlanTempus.Components.Users.Create
{
public class CreateUserHandler(IDatabaseOperations databaseOperations, ISecureTokenizer secureTokenizer)
- {
- private readonly ISecureTokenizer _secureTokenizer;
-
- public async Task Handle(CreateUserCommand command)
- {
- using var db = databaseOperations.CreateScope(nameof(CreateUserHandler));
- try
- {
- var sql = @"
+ {
+ public async Task Handle(CreateUserCommand command)
+ {
+ using var db = databaseOperations.CreateScope(nameof(CreateUserHandler));
+ try
+ {
+ var sql = @"
INSERT INTO system.users(email, password_hash, security_stamp, email_confirmed,
- access_failed_count, lockout_enabled, lockout_end,
- is_active, created_at, last_login_at)
+ access_failed_count, lockout_enabled,
+ is_active)
VALUES(@Email, @PasswordHash, @SecurityStamp, @EmailConfirmed,
- @AccessFailedCount, @LockoutEnabled, @LockoutEnd,
- @IsActive, @CreatedAt, @LastLoginAt)
- RETURNING id, created_at";
+ @AccessFailedCount, @LockoutEnabled, @IsActive)
+ RETURNING id, created_at, email, is_active";
- var result = await db.Connection.QuerySqlAsync(sql, new
- {
- Email = command.Email,
- PasswordHash = secureTokenizer.TokenizeText(command.Password),
- SecurityStamp = Guid.NewGuid().ToString("N"),
- EmailConfirmed = false,
- AccessFailedCount = 0,
- LockoutEnabled = true,
- LockoutEnd = (DateTime?)null,
- IsActive = command.IsActive,
- CreatedAt = DateTime.UtcNow,
- LastLoginAt = (DateTime?)null
- });
+ var data = await db.Connection.QuerySqlAsync(sql, new
+ {
+ Email = command.Email,
+ PasswordHash = secureTokenizer.TokenizeText(command.Password),
+ SecurityStamp = Guid.NewGuid().ToString("N"),
+ EmailConfirmed = false,
+ AccessFailedCount = 0,
+ LockoutEnabled = false,
+ IsActive = command.IsActive,
+ });
- var createdUser = result.First();
- db.Success();
+ db.Success();
- return new CreateUserResponse
- {
- Id = createdUser.Id,
- Email = command.Email,
- IsActive = command.IsActive,
- CreatedAt = createdUser.CreatedAt
- };
- }
- catch (Exception ex)
- {
- db.Error(ex);
- throw;
- }
- }
-
- private class UserCreationResult
- {
- public long Id { get; set; }
- public DateTime CreatedAt { get; set; }
- }
- }
+ return data.First();
+ }
+ catch (Exception ex)
+ {
+ db.Error(ex);
+ throw;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/PlanTempus.Components/Users/Create/CreateUserResponse.cs b/PlanTempus.Components/Users/Create/CreateUserResult.cs
similarity index 84%
rename from PlanTempus.Components/Users/Create/CreateUserResponse.cs
rename to PlanTempus.Components/Users/Create/CreateUserResult.cs
index defdbd4..24c92c8 100644
--- a/PlanTempus.Components/Users/Create/CreateUserResponse.cs
+++ b/PlanTempus.Components/Users/Create/CreateUserResult.cs
@@ -1,7 +1,7 @@
namespace PlanTempus.Components.Users.Create
{
- public class CreateUserResponse
+ public class CreateUserResult
{
public long Id { get; set; }
public string Email { get; set; }
diff --git a/SetupInfrastructure/Program.cs b/SetupInfrastructure/Program.cs
index bea1499..a50e496 100644
--- a/SetupInfrastructure/Program.cs
+++ b/SetupInfrastructure/Program.cs
@@ -170,8 +170,8 @@ namespace PlanTempus.SetupInfrastructure
string.IsNullOrEmpty(userPass.Split(":")[1]));
var superUser = new ConnectionStringParameters(
- user: userPass.Split(":")[0],
- pwd: userPass.Split(":")[1]
+ User: userPass.Split(":")[0],
+ Pwd: userPass.Split(":")[1]
);
if (IsSuperAdmin(superUser))
@@ -191,7 +191,7 @@ namespace PlanTempus.SetupInfrastructure
sw.Restart();
//use application user, we use that role from now.
- var connParams = new ConnectionStringParameters(user: "heimdall", pwd: "3911");
+ var connParams = new ConnectionStringParameters(User: "heimdall", Pwd: "3911");
_setupIdentitySystem.With(new SetupIdentitySystem.Command { Schema = "system" }, connParams);
Console.WriteLine($"DONE, took: {sw.ElapsedMilliseconds} ms");
diff --git a/SqlManagement/.dbeaver/.project-metadata.json.bak b/SqlManagement/.dbeaver/.project-metadata.json.bak
index c8ced2b..3200961 100644
--- a/SqlManagement/.dbeaver/.project-metadata.json.bak
+++ b/SqlManagement/.dbeaver/.project-metadata.json.bak
@@ -1 +1 @@
-{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/Script.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/SmartConfigSystem.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44"}}}
\ No newline at end of file
+{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/Script.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/SmartConfigSystem.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"}}}
\ No newline at end of file
diff --git a/qodana.yaml b/qodana.yaml
new file mode 100644
index 0000000..178e06f
--- /dev/null
+++ b/qodana.yaml
@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------------#
+# Qodana analysis is configured by qodana.yaml file #
+# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
+#-------------------------------------------------------------------------------#
+version: "1.0"
+
+#Specify IDE code to run analysis without container (Applied in CI/CD pipeline)
+ide: QDNET
+
+#Specify inspection profile for code analysis
+profile:
+ name: qodana.starter
+
+#Enable inspections
+#include:
+# - name:
+
+#Disable inspections
+#exclude:
+# - name:
+# paths:
+# -
+
+#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+#bootstrap: sh ./prepare-qodana.sh
+
+#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+#plugins:
+# - id: #(plugin id can be found at https://plugins.jetbrains.com)