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

271 lines
9.8 KiB
C#

using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using DramaLing.Api.Services;
using DramaLing.Api.Services.AI;
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 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() &&
string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GEMINI_API_KEY")))
{
builder.Services.PostConfigure<GeminiOptions>(options =>
{
if (string.IsNullOrEmpty(options.ApiKey))
{
options.ApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY")
?? builder.Configuration["Gemini: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;
});
// Entity Framework - 使用 SQLite 進行測試
var useInMemoryDb = Environment.GetEnvironmentVariable("USE_INMEMORY_DB") == "true";
if (useInMemoryDb)
{
builder.Services.AddDbContext<DramaLingDbContext>(options =>
options.UseSqlite("Data Source=:memory:"));
}
else
{
var connectionString = Environment.GetEnvironmentVariable("DRAMALING_DB_CONNECTION")
?? builder.Configuration.GetConnectionString("DefaultConnection")
?? "Data Source=dramaling_test.db"; // SQLite 檔案
builder.Services.AddDbContext<DramaLingDbContext>(options =>
options.UseSqlite(connectionString));
}
// 暫時註解新的服務,等修正編譯錯誤後再啟用
// Repository Services
// builder.Services.AddScoped(typeof(IRepository<>), typeof(BaseRepository<>));
// builder.Services.AddScoped<IFlashcardRepository, SimpleFlashcardRepository>();
// builder.Services.AddScoped<IUserRepository, UserRepository>();
// Caching Services
builder.Services.AddMemoryCache();
builder.Services.AddScoped<ICacheService, HybridCacheService>();
// AI Services
// builder.Services.AddHttpClient<GeminiAIProvider>();
// builder.Services.AddScoped<IAIProvider, GeminiAIProvider>();
// builder.Services.AddScoped<IAIProviderManager, AIProviderManager>();
// Custom Services
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddHttpClient<IGeminiService, GeminiService>();
// 新增帶快取的分析服務
builder.Services.AddScoped<IAnalysisService, AnalysisService>();
builder.Services.AddScoped<IUsageTrackingService, UsageTrackingService>();
builder.Services.AddScoped<IAzureSpeechService, AzureSpeechService>();
// 智能填空題系統服務
builder.Services.AddScoped<IWordVariationService, WordVariationService>();
builder.Services.AddScoped<IBlankGenerationService, BlankGenerationService>();
builder.Services.AddScoped<IAudioCacheService, AudioCacheService>();
// 🆕 智能複習服務註冊
builder.Services.Configure<SpacedRepetitionOptions>(
builder.Configuration.GetSection(SpacedRepetitionOptions.SectionName));
builder.Services.AddScoped<ISpacedRepetitionService, SpacedRepetitionService>();
builder.Services.AddScoped<IReviewTypeSelectorService, ReviewTypeSelectorService>();
builder.Services.AddScoped<IQuestionGeneratorService, QuestionGeneratorService>();
// 🆕 學習會話服務註冊
builder.Services.AddScoped<IStudySessionService, StudySessionService>();
builder.Services.AddScoped<IReviewModeSelector, ReviewModeSelector>();
// 🆕 選項詞彙庫服務註冊
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>();
// Image Generation Services
builder.Services.AddHttpClient<IReplicateService, ReplicateService>();
builder.Services.AddScoped<IImageGenerationOrchestrator, ImageGenerationOrchestrator>();
// Image Storage Services
builder.Services.AddScoped<IImageStorageService, LocalImageStorageService>();
// Image Processing Services
builder.Services.AddScoped<IImageProcessingService, ImageProcessingService>();
// 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();