using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Services; using DramaLing.Api.Services.Caching; using DramaLing.Api.Services.Monitoring; using DramaLing.Api.Services.Storage; using DramaLing.Api.Middleware; using DramaLing.Api.Models.Configuration; using DramaLing.Api.Repositories; using DramaLing.Api.Extensions; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Options; using Microsoft.Extensions.FileProviders; using System.Text; var builder = WebApplication.CreateBuilder(args); // ✅ 配置管理:強型別配置和驗證 builder.Services.Configure( builder.Configuration.GetSection(GeminiOptions.SectionName)); builder.Services.AddSingleton, GeminiOptionsValidator>(); // 新增 Replicate 配置 builder.Services.Configure( builder.Configuration.GetSection(ReplicateOptions.SectionName)); builder.Services.AddSingleton, ReplicateOptionsValidator>(); // 在開發環境設定 API Key - 修復配置讀取邏輯 if (builder.Environment.IsDevelopment()) { builder.Services.PostConfigure(options => { // 只有當 ApiKey 未設置時才嘗試從其他來源讀取 if (string.IsNullOrEmpty(options.ApiKey)) { // 優先從環境變數讀取 var envApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); if (!string.IsNullOrEmpty(envApiKey)) { options.ApiKey = envApiKey; } // 如果環境變數沒有,Configuration 應該已經包含 user-secrets // 這裡只是作為後備,不應該覆蓋已經從 user-secrets 載入的設定 else if (string.IsNullOrEmpty(builder.Configuration["Gemini:ApiKey"])) { // 只有在真的沒有任何配置時才使用測試金鑰 options.ApiKey = "test-key"; } } }); } // Add services to the container. builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; options.JsonSerializerOptions.WriteIndented = true; }); // 配置資料庫服務 builder.Services.AddDatabaseServices(builder.Configuration); // 配置 Repository 和 Caching 服務 builder.Services.AddRepositoryServices(); builder.Services.AddCachingServices(); // 配置 AI 和業務服務 builder.Services.AddAIServices(builder.Configuration); builder.Services.AddBusinessServices(); // 🆕 選項詞彙庫服務註冊 builder.Services.Configure( builder.Configuration.GetSection(OptionsVocabularyOptions.SectionName)); builder.Services.AddSingleton, OptionsVocabularyOptionsValidator>(); builder.Services.AddSingleton(); // 監控指標服務 // builder.Services.AddScoped(); // 暫時註解,使用固定選項 builder.Services.AddScoped(); // (圖片相關服務已透過 AddAIServices 和 AddBusinessServices 註冊) // Background Services (快取清理服務已移除) // Authentication - 從環境變數讀取 JWT 配置 var supabaseUrl = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_URL") ?? builder.Configuration["Supabase:Url"] ?? "https://localhost"; var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_JWT_SECRET") ?? builder.Configuration["Supabase:JwtSecret"] ?? "dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = supabaseUrl, ValidAudience = "authenticated", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)) }; }); // CORS for frontend builder.Services.AddCors(options => { options.AddPolicy("AllowFrontend", policy => { policy.WithOrigins("http://localhost:3000", "http://localhost:3001", "http://localhost:3002") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials() .SetPreflightMaxAge(TimeSpan.FromMinutes(5)); }); // 開發環境允許所有來源 options.AddPolicy("AllowAll", policy => { policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new() { Title = "DramaLing API", Version = "v1" }); // JWT Authentication for Swagger c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme", Name = "Authorization", In = Microsoft.OpenApi.Models.ParameterLocation.Header, Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey, Scheme = "Bearer" }); c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement { { new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Reference = new Microsoft.OpenApi.Models.OpenApiReference { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); var app = builder.Build(); // Configure the HTTP request pipeline. // 全域錯誤處理中介軟體 (保持原有的) app.UseMiddleware(); // TODO: 新的中間件需要修正編譯錯誤後再啟用 // app.UseMiddleware(); // app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "DramaLing API v1"); c.RoutePrefix = "swagger"; }); } // 開發環境使用寬鬆的 CORS 政策 if (app.Environment.IsDevelopment()) { app.UseCors("AllowAll"); } else { app.UseCors("AllowFrontend"); } app.UseHttpsRedirection(); // 開發環境靜態檔案服務 (暫時用,生產時會使用雲端 CDN) if (app.Environment.IsDevelopment()) { app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")), RequestPath = "/images" }); } app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); // Health check endpoint app.MapGet("/health", () => new { Status = "Healthy", Timestamp = DateTime.UtcNow }); // 確保資料庫已創建 using (var scope = app.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); try { context.Database.EnsureCreated(); app.Logger.LogInformation("Database ensured created - Using fixed vocabulary options"); } catch (Exception ex) { app.Logger.LogError(ex, "Error creating database"); } } app.Run(); // 使 Program 類別對測試專案可見 public partial class Program { }