diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fbcd972 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +name: legal-assistant + +services: + web-api: + image: ${DOCKER_REGISTRY-}webapi + container_name: web-api + build: + context: . + dockerfile: src/Web.Api/Dockerfile + ports: + - 10000:8080 # HTTP + - 10001:8081 # HTTPS + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__DefaultConnection=Host=postgres;Database=legal-assistant;Username=postgres;Password=postgres;Port=5432 + depends_on: + - postgres + + postgres: + image: postgres:17 + container_name: postgres + environment: + - POSTGRES_DB=legal-assistant + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - TZ=UTC + - PGTZ=UTC + volumes: + - ./.containers/db:/var/lib/postgresql/data + ports: + - 5432:5432 + + # seq: + # image: datalust/seq:2024.3 + # container_name: seq + # environment: + # - ACCEPT_EULA=Y + # ports: + # - 8081:80 diff --git a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs index cfc53de..0fbdd5f 100644 --- a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -46,15 +46,15 @@ private static IServiceCollection AddDatabase( IConfiguration configuration) { var connectionString = configuration.GetConnectionString("DefaultConnection") - ?? "Server=(localdb)\\mssqllocaldb;Database=LegalAssistantDb;Trusted_Connection=true;MultipleActiveResultSets=true"; + ?? "Host=localhost;Database=legal-assistant;Username=postgres;Password=postgres"; services.AddDbContext(options => { - options.UseSqlServer(connectionString, sqlOptions => - sqlOptions.EnableRetryOnFailure( + options.UseNpgsql(connectionString, npgsqlOptions => + npgsqlOptions.EnableRetryOnFailure( maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(30), - errorNumbersToAdd: null)); + errorCodesToAdd: null)); #if DEBUG options.EnableSensitiveDataLogging(); @@ -109,16 +109,10 @@ private static IServiceCollection AddExternalServices( this IServiceCollection services, IConfiguration configuration) { - // Email Service - services.Configure(configuration.GetSection("EmailSettings")); - - // File Storage - services.Configure(configuration.GetSection("FileStorageSettings")); - // JWT Settings services.Configure(configuration.GetSection("JwtSettings")); - // TODO: Add external service clients + // TODO: Add external service clients when needed // services.AddHttpClient(); return services; @@ -156,7 +150,7 @@ public static IServiceCollection AddHealthChecks( var connectionString = configuration.GetConnectionString("DefaultConnection"); if (!string.IsNullOrEmpty(connectionString)) { - healthChecks.AddSqlServer(connectionString, name: "database"); + healthChecks.AddNpgSql(connectionString, name: "database"); } // Redis health check (if configured) diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index b92a4b1..284f370 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,7 +21,7 @@ - + diff --git a/src/Web.Api/Controllers/V1/HealthController.cs b/src/Web.Api/Controllers/V1/HealthController.cs new file mode 100644 index 0000000..a18bc30 --- /dev/null +++ b/src/Web.Api/Controllers/V1/HealthController.cs @@ -0,0 +1,197 @@ +using Microsoft.AspNetCore.Mvc; +using System.Diagnostics; +using System.Reflection; +using System.Security.Cryptography; + +namespace Web.Api.Controllers.V1; + +/// +/// Health check controller for system monitoring and testing +/// +[ApiController] +[Route("api/v{version:apiVersion}/[controller]")] +[ApiVersion("1.0")] +public sealed class HealthController : BaseController +{ + private readonly ILogger _logger; + + public HealthController(ILogger logger) + { + _logger = logger; + } + + /// + /// Basic health check endpoint + /// + /// System health status + [HttpGet] + [ProducesResponseType(typeof(HealthResponse), StatusCodes.Status200OK)] + public IActionResult GetHealth() + { + _logger.LogInformation("Health check requested"); + + var response = new HealthResponse + { + Status = "Healthy", + Timestamp = DateTime.UtcNow, + Version = GetApplicationVersion(), + Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production", + MachineName = Environment.MachineName, + ProcessId = Environment.ProcessId, + UpTime = GetUpTime() + }; + + return Ok(response); + } + + /// + /// Detailed system information for testing + /// + /// Detailed system information + [HttpGet("detailed")] + [ProducesResponseType(typeof(DetailedHealthResponse), StatusCodes.Status200OK)] + public IActionResult GetDetailedHealth() + { + _logger.LogInformation("Detailed health check requested"); + + var response = new DetailedHealthResponse + { + Status = "Healthy", + Timestamp = DateTime.UtcNow, + Version = GetApplicationVersion(), + Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production", + MachineName = Environment.MachineName, + ProcessId = Environment.ProcessId, + UpTime = GetUpTime(), + SystemInfo = new SystemInfo + { + OperatingSystem = Environment.OSVersion.ToString(), + ProcessorCount = Environment.ProcessorCount, + WorkingSet = Environment.WorkingSet, + RuntimeVersion = Environment.Version.ToString(), + CurrentDirectory = Environment.CurrentDirectory + }, + Services = new ServicesStatus + { + Database = "Connected", // TODO: Check actual database connection + Cache = "Available", // TODO: Check Redis if configured + ExternalApis = "Online" // TODO: Check external services + } + }; + + return Ok(response); + } + + /// + /// Simple ping endpoint for quick availability check + /// + /// Pong response + [HttpGet("ping")] + [ProducesResponseType(typeof(PingResponse), StatusCodes.Status200OK)] + public IActionResult Ping() + { + return Ok(new PingResponse + { + Message = "Pong", + Timestamp = DateTime.UtcNow, + RequestId = HttpContext.TraceIdentifier + }); + } + + /// + /// Test endpoint that always returns success for testing purposes + /// + /// Test success response + [HttpGet("test")] + [ProducesResponseType(typeof(TestResponse), StatusCodes.Status200OK)] + public IActionResult Test() + { + _logger.LogInformation("Test endpoint called"); + + return Ok(new TestResponse + { + Success = true, + Message = "Legal Assistant API is working correctly!", + Timestamp = DateTime.UtcNow, + TestData = new + { + RandomNumber = RandomNumberGenerator.GetInt32(1, 1000), + CurrentUser = User?.Identity?.Name ?? "Anonymous", + Headers = Request.Headers.Count, + Request.Method, + Path = Request.Path.Value + } + }); + } + + private static string GetApplicationVersion() + { + var assembly = Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + return version?.ToString() ?? "Unknown"; + } + + private static TimeSpan GetUpTime() + { + return DateTime.UtcNow - Process.GetCurrentProcess().StartTime.ToUniversalTime(); + } +} + +#region Response Models + +public sealed record HealthResponse +{ + public required string Status { get; init; } + public required DateTime Timestamp { get; init; } + public required string Version { get; init; } + public required string Environment { get; init; } + public required string MachineName { get; init; } + public required int ProcessId { get; init; } + public required TimeSpan UpTime { get; init; } +} + +public sealed record DetailedHealthResponse +{ + public required string Status { get; init; } + public required DateTime Timestamp { get; init; } + public required string Version { get; init; } + public required string Environment { get; init; } + public required string MachineName { get; init; } + public required int ProcessId { get; init; } + public required TimeSpan UpTime { get; init; } + public required SystemInfo SystemInfo { get; init; } + public required ServicesStatus Services { get; init; } +} + +public sealed record SystemInfo +{ + public required string OperatingSystem { get; init; } + public required int ProcessorCount { get; init; } + public required long WorkingSet { get; init; } + public required string RuntimeVersion { get; init; } + public required string CurrentDirectory { get; init; } +} + +public sealed record ServicesStatus +{ + public required string Database { get; init; } + public required string Cache { get; init; } + public required string ExternalApis { get; init; } +} + +public sealed record PingResponse +{ + public required string Message { get; init; } + public required DateTime Timestamp { get; init; } + public required string RequestId { get; init; } +} + +public sealed record TestResponse +{ + public required bool Success { get; init; } + public required string Message { get; init; } + public required DateTime Timestamp { get; init; } + public required object TestData { get; init; } +} + +#endregion diff --git a/src/Web.Api/Dockerfile b/src/Web.Api/Dockerfile new file mode 100644 index 0000000..fa5a7f9 --- /dev/null +++ b/src/Web.Api/Dockerfile @@ -0,0 +1,28 @@ +# This Docker file uses the .NET 8 runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Directory.Build.props", "."] +COPY ["src/Web.Api/Web.Api.csproj", "src/Web.Api/"] +COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"] +COPY ["src/Application/Application.csproj", "src/Application/"] +COPY ["src/Domain/Domain.csproj", "src/Domain/"] +RUN dotnet restore "./src/Web.Api/Web.Api.csproj" +COPY . . +WORKDIR "/src/src/Web.Api" +RUN dotnet build "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Web.Api.dll"] \ No newline at end of file diff --git a/src/Web.Api/Program.cs b/src/Web.Api/Program.cs index 7342ed6..56d53ef 100644 --- a/src/Web.Api/Program.cs +++ b/src/Web.Api/Program.cs @@ -64,7 +64,11 @@ // Add Global Exception Middleware app.UseMiddleware(); -app.UseHttpsRedirection(); +// Only use HTTPS redirection in production or when HTTPS is properly configured +if (app.Environment.IsProduction() || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_HTTPS_PORT"))) +{ + app.UseHttpsRedirection(); +} // Add CORS app.UseCors("DefaultPolicy"); @@ -76,9 +80,14 @@ // Map controllers app.MapControllers(); -// Map Health Checks +// Map Health Checks endpoint +// Health endpoint for monitoring and load balancer to check application health +// Returns 200 OK if app is healthy, 503 Service Unavailable if there are issues app.MapHealthChecks("/health"); +// Automatically redirect to Swagger UI +app.MapGet("/", () => Results.Redirect("/swagger")); + // Ensure database is created await EnsureDatabaseCreatedAsync(app); diff --git a/src/Web.Api/Properties/launchSettings.json b/src/Web.Api/Properties/launchSettings.json index 1626fea..d486cd3 100644 --- a/src/Web.Api/Properties/launchSettings.json +++ b/src/Web.Api/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5221", + "applicationUrl": "http://localhost:10000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -24,7 +24,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7260;http://localhost:5221", + "applicationUrl": "https://localhost:10001;http://localhost:10000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Web.Api/appsettings.Development.json b/src/Web.Api/appsettings.Development.json index 0c208ae..97ba039 100644 --- a/src/Web.Api/appsettings.Development.json +++ b/src/Web.Api/appsettings.Development.json @@ -4,5 +4,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Database=legal-assistant;Username=postgres;Password=postgres;Port=5432" } } diff --git a/src/Web.Api/appsettings.json b/src/Web.Api/appsettings.json index 2fbf78c..5d10f24 100644 --- a/src/Web.Api/appsettings.json +++ b/src/Web.Api/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=LegalAssistantDb;Trusted_Connection=true;MultipleActiveResultSets=true;", + "DefaultConnection": "Host=localhost;Database=legal-assistant;Username=postgres;Password=postgres;Port=5432", "Redis": "" }, "JwtSettings": { @@ -16,22 +16,5 @@ "Audience": "LegalAssistantUsers", "ExpirationHours": 1, "RefreshTokenExpirationDays": 7 - }, - "EmailSettings": { - "SmtpServer": "smtp.gmail.com", - "SmtpPort": 587, - "Username": "", - "Password": "", - "FromEmail": "noreply@legalassistant.com", - "FromName": "Legal Assistant", - "EnableSsl": true - }, - "FileStorageSettings": { - "Provider": "Local", - "ConnectionString": "", - "ContainerName": "files", - "BasePath": "uploads", - "MaxFileSizeBytes": 10485760, - "AllowedExtensions": [".pdf", ".docx", ".png", ".jpg", ".jpeg"] } }