dramaling-vocab-learning/backend/DramaLing.Api/Program.cs

232 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<GeminiOptions>(
builder.Configuration.GetSection(GeminiOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<GeminiOptions>, GeminiOptionsValidator>();
// 新增 Replicate 配置
builder.Services.Configure<ReplicateOptions>(
builder.Configuration.GetSection(ReplicateOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<ReplicateOptions>, ReplicateOptionsValidator>();
// 在開發環境設定 API Key - 修復配置讀取邏輯
if (builder.Environment.IsDevelopment())
{
builder.Services.PostConfigure<GeminiOptions>(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<OptionsVocabularyOptions>(
builder.Configuration.GetSection(OptionsVocabularyOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<OptionsVocabularyOptions>, OptionsVocabularyOptionsValidator>();
builder.Services.AddSingleton<OptionsVocabularyMetrics>(); // 監控指標服務
// builder.Services.AddScoped<OptionsVocabularySeeder>(); // 暫時註解,使用固定選項
builder.Services.AddScoped<IOptionsVocabularyService, OptionsVocabularyService>();
// (圖片相關服務已透過 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<string>()
}
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
// 全域錯誤處理中介軟體 (保持原有的)
app.UseMiddleware<ErrorHandlingMiddleware>();
// TODO: 新的中間件需要修正編譯錯誤後再啟用
// app.UseMiddleware<SecurityMiddleware>();
// app.UseMiddleware<AdvancedErrorHandlingMiddleware>();
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<DramaLingDbContext>();
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 { }