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)