Initial project import
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
using System.Text;
|
||||
using System.Threading.RateLimiting;
|
||||
using MiaoJiZhang.Api.Services;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using MiaoJiZhang.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.AddPolicy("auth", context =>
|
||||
RateLimitPartition.GetFixedWindowLimiter(
|
||||
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
_ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 10,
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
QueueLimit = 0,
|
||||
}));
|
||||
options.AddPolicy("ai", context =>
|
||||
RateLimitPartition.GetConcurrencyLimiter(
|
||||
context.User.FindFirst("sub")?.Value ??
|
||||
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
_ => new ConcurrencyLimiterOptions
|
||||
{
|
||||
PermitLimit = 2,
|
||||
QueueLimit = 0,
|
||||
}));
|
||||
options.AddPolicy("upload", context =>
|
||||
RateLimitPartition.GetFixedWindowLimiter(
|
||||
context.User.FindFirst("sub")?.Value ??
|
||||
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
_ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 10,
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
QueueLimit = 0,
|
||||
}));
|
||||
});
|
||||
builder.Services.AddSingleton<JwtService>();
|
||||
builder.Services.AddScoped<AgentService>();
|
||||
builder.Services.AddScoped<BudgetRecommendationService>();
|
||||
builder.Services.AddScoped<ReplyService>();
|
||||
builder.Services.AddScoped<LedgerResolver>();
|
||||
builder.Services.AddScoped<AiPermissionService>();
|
||||
builder.Services.AddScoped<AiChatQuotaService>();
|
||||
builder.Services.AddScoped<AiPermissionFilter>();
|
||||
builder.Services.AddHttpClient("LlmClient");
|
||||
builder.Services.AddSingleton<OpenAiVisionClient>();
|
||||
builder.Services.AddSingleton<ILlmClient>(sp => sp.GetRequiredService<OpenAiVisionClient>());
|
||||
builder.Services.AddHostedService<RecycleBinCleanupService>();
|
||||
builder.Services.AddScoped<AccountDataEraser>();
|
||||
builder.Services.AddHostedService<AccountClosureCleanupService>();
|
||||
|
||||
var conn = builder.Configuration.GetConnectionString("Default");
|
||||
if (string.IsNullOrWhiteSpace(conn))
|
||||
throw new InvalidOperationException("必须通过 ConnectionStrings__Default 配置数据库连接");
|
||||
var jwtSecret = builder.Configuration["Jwt:Secret"];
|
||||
if (string.IsNullOrWhiteSpace(jwtSecret) || jwtSecret.Length < 32)
|
||||
throw new InvalidOperationException("必须通过 Jwt__Secret 配置至少 32 位的 JWT 密钥");
|
||||
var adminKey = builder.Configuration["Admin:Key"];
|
||||
if (string.IsNullOrWhiteSpace(adminKey) || adminKey.Length < 24)
|
||||
throw new InvalidOperationException("必须通过 Admin__Key 配置至少 24 位的管理密钥");
|
||||
builder.Services.AddDbContext<AppDbContext>(o =>
|
||||
o.UseMySql(conn, ServerVersion.AutoDetect(conn)));
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(o =>
|
||||
{
|
||||
o.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
||||
ValidAudience = builder.Configuration["Jwt:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(jwtSecret)),
|
||||
};
|
||||
o.Events = new JwtBearerEvents
|
||||
{
|
||||
OnTokenValidated = async context =>
|
||||
{
|
||||
var idValue = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
||||
?? context.Principal?.FindFirst("sub")?.Value;
|
||||
if (!long.TryParse(idValue, out var userId))
|
||||
{
|
||||
context.Fail("Invalid user identity");
|
||||
return;
|
||||
}
|
||||
|
||||
var versionValue = context.Principal?.FindFirst("auth_version")?.Value;
|
||||
var tokenVersion = int.TryParse(versionValue, out var parsedVersion)
|
||||
? parsedVersion
|
||||
: 0;
|
||||
var db = context.HttpContext.RequestServices.GetRequiredService<AppDbContext>();
|
||||
var userState = await db.Users
|
||||
.AsNoTracking()
|
||||
.Where(user => user.Id == userId)
|
||||
.Select(user => new
|
||||
{
|
||||
user.AuthVersion,
|
||||
user.IsBanned,
|
||||
user.AccountClosureScheduledAt,
|
||||
})
|
||||
.FirstOrDefaultAsync(context.HttpContext.RequestAborted);
|
||||
if (userState is null || userState.IsBanned ||
|
||||
userState.AccountClosureScheduledAt != null ||
|
||||
userState.AuthVersion != tokenVersion)
|
||||
{
|
||||
context.Fail("Account session has been revoked");
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.MigrateAsync();
|
||||
if (app.Environment.IsDevelopment())
|
||||
await DbSeeder.SeedAsync(db);
|
||||
}
|
||||
if (app.Environment.IsDevelopment())
|
||||
app.MapOpenApi();
|
||||
|
||||
var buildTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") + " UTC";
|
||||
var apiVersion = builder.Configuration["Build:Version"] ?? "dev";
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseRateLimiter();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapGet("/api/ping", () => Results.Ok(new { status = "ok", version = apiVersion, built = buildTime }));
|
||||
app.MapGet("/api/version", () => Results.Ok(new { app = "记之 API", version = apiVersion, built = buildTime }));
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program;
|
||||
|
||||
Reference in New Issue
Block a user