diff --git a/BimAI.API/BimAI.API.csproj b/BimAI.API/BimAI.API.csproj index 2ac61ec..2374ce7 100644 --- a/BimAI.API/BimAI.API.csproj +++ b/BimAI.API/BimAI.API.csproj @@ -8,6 +8,9 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/BimAI.API/Program.cs b/BimAI.API/Program.cs index b0c7e75..1c72fed 100644 --- a/BimAI.API/Program.cs +++ b/BimAI.API/Program.cs @@ -1,7 +1,10 @@ using System.Text; using BimAI.API.Services; using BimAI.Infrastructure.Data; +using BimAI.Infrastructure.Jobs; using BimAI.Infrastructure.Sync; +using Hangfire; +using Hangfire.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; @@ -18,6 +21,29 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// Start Hangfire section +builder.Services.AddHangfire(configuration => configuration + .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"), + new SqlServerStorageOptions + { + CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), + SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), + QueuePollInterval = TimeSpan.Zero, + UseRecommendedIsolationLevel = true, + DisableGlobalLocks = true, + SchemaName = "Hangfire" + } + ) +); +builder.Services.AddHangfireServer(options => +{ + options.ServerName = builder.Configuration["Hangfire:ServerName"]; + options.WorkerCount = builder.Configuration.GetValue("Hangfire:WorkerCount", 5); +}); +// End Hangfire section // Start auth section var jwtSettings = builder.Configuration.GetSection("JwtSettings"); var secretKey = jwtSettings["SecretKey"]; @@ -65,7 +91,22 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); app.UseCors("AllowAll"); +app.UseHangfireDashboard(builder.Configuration["Hangfire:DashboardPath"] ?? "/hangfire", new DashboardOptions +{ + AsyncAuthorization = new[] { new HangfireAuthorizationFilter() }, + DashboardTitle = "BimAI - Job Dashboard" +}); app.UseAuthorization(); app.UseAuthorization(); app.MapControllers(); + +RecurringJob.AddOrUpdate( + "product-sync", + job => job.ExecuteAsync(), + Cron.Daily(2, 0), // Every day at 2:00 AM + new RecurringJobOptions + { + TimeZone = TimeZoneInfo.Local + }); + app.Run(); \ No newline at end of file diff --git a/BimAI.API/Services/HangfireAuthorizationFilter.cs b/BimAI.API/Services/HangfireAuthorizationFilter.cs new file mode 100644 index 0000000..23e563b --- /dev/null +++ b/BimAI.API/Services/HangfireAuthorizationFilter.cs @@ -0,0 +1,20 @@ +using Hangfire.Dashboard; + +namespace BimAI.API.Services; + +public class HangfireAuthorizationFilter: IDashboardAsyncAuthorizationFilter +{ + public Task AuthorizeAsync(DashboardContext context) + { + var httpContext = context.GetHttpContext(); + + var env = httpContext.RequestServices.GetService(); + if (env.IsDevelopment()) + { + return Task.FromResult(true); + } + + var isAuthenticated = httpContext.User.Identity?.IsAuthenticated ?? false; + return Task.FromResult(isAuthenticated); + } +} \ No newline at end of file diff --git a/BimAI.API/appsettings.Development.json b/BimAI.API/appsettings.Development.json index 15779f6..4030be7 100644 Binary files a/BimAI.API/appsettings.Development.json and b/BimAI.API/appsettings.Development.json differ diff --git a/BimAI.Infrastructure/Jobs/ProductSyncJob.cs b/BimAI.Infrastructure/Jobs/ProductSyncJob.cs new file mode 100644 index 0000000..94ebbe1 --- /dev/null +++ b/BimAI.Infrastructure/Jobs/ProductSyncJob.cs @@ -0,0 +1,31 @@ +using BimAI.Infrastructure.Sync; +using Microsoft.Extensions.Logging; + +namespace BimAI.Infrastructure.Jobs; + +public class ProductSyncJob +{ + private readonly ProductSyncService _productSyncService; + private readonly ILogger _logger; + + public ProductSyncJob(ProductSyncService productSyncService, ILogger logger) + { + _productSyncService = productSyncService; + _logger = logger; + } + + public async Task ExecuteAsync() + { + _logger.LogInformation("Starting product sync..."); + + try + { + await _productSyncService.RunAsync(); + _logger.LogInformation("Product sync finished."); + } catch (Exception ex) + { + _logger.LogError(ex, "Error during product sync."); + throw; + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..96ad1b5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +services: + mssql: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: bimai-mssql + hostname: bimai-mssql + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=BimAI_Dev_Pass_2024! + - MSSQL_PID=Developer + ports: + - "1433:1433" + volumes: + - mssql-data:/var/opt/mssql + - ./docker/mssql/init:/docker-entrypoint-initdb.d + networks: + - bimai-network + healthcheck: + test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "BimAI_Dev_Pass_2024!" -Q "SELECT 1" || exit 1 + interval: 10s + timeout: 3s + retries: 10 + start_period: 10s + + mongodb: + image: mongo:7.0 + container_name: bimai-mongodb + hostname: bimai-mongodb + environment: + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=BimAI_Mongo_2024! + - MONGO_INITDB_DATABASE=bimai + ports: + - "27017:27017" + volumes: + - mongodb-data:/data/db + - mongodb-config:/data/configdb + - ./docker/mongodb/init:/docker-entrypoint-initdb.d + networks: + - bimai-network + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +networks: + bimai-network: + driver: bridge + +volumes: + mssql-data: + name: bimai-mssql-data + mongodb-data: + name: bimai-mongodb-data + mongodb-config: + name: bimai-mongodb-config \ No newline at end of file