Starter experience so the system is usable immediately after git clone → migrate → run. DemoCatalogSeeder (Development only, runs once — skips if tenant has any products): - 8 product groups: Напитки (Безалкогольные, Алкогольные), Молочка, Хлеб, Кондитерские, Бакалея, Снеки — hierarchical Path computed - 2 demo suppliers: ТОО «Продтрейд» (legal entity, KZ BIN, bank details), ИП Иванов (individual) - 35 realistic KZ-market products with: - Demo barcodes in 2xxx internal range (won't collide with real products) - Retail price + purchase price at 72% of retail - Country of origin (KZ / RU) - Хлеб marked as 0% VAT (socially important goods in KZ) - Сыр «Российский» marked as весовой - Articles in kebab-case: DR-SOD-001, DAI-MLK-002, SW-CHO-001 etc. Product form (full page /catalog/products/new and /:id, not modal): - 5 sections: Основное / Классификация / Остатки и закупка / Цены / Штрихкоды - Dropdowns for unit, VAT, group, country, supplier, currency via useLookups hooks - Defaults pre-filled for new product (default VAT, base unit, KZT) - Prices table: add/remove rows, pick price type + amount + currency - Barcodes table: EAN-13/8/CODE128/UPC options, "primary" enforces single - Server-side atomic save (existing Prices+Barcodes replaced on PUT) Products page: row click → edit page, Add button → new page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
149 lines
4.9 KiB
C#
149 lines
4.9 KiB
C#
using System.Security.Claims;
|
|
using foodmarket.Api.Infrastructure.Tenancy;
|
|
using foodmarket.Api.Seed;
|
|
using foodmarket.Application.Common.Tenancy;
|
|
using foodmarket.Infrastructure.Identity;
|
|
using foodmarket.Infrastructure.Persistence;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using OpenIddict.Validation.AspNetCore;
|
|
using Serilog;
|
|
using static OpenIddict.Abstractions.OpenIddictConstants;
|
|
|
|
Log.Logger = new LoggerConfiguration()
|
|
.WriteTo.Console()
|
|
.CreateBootstrapLogger();
|
|
|
|
try
|
|
{
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Host.UseSerilog((ctx, services, cfg) =>
|
|
cfg.ReadFrom.Configuration(ctx.Configuration).ReadFrom.Services(services));
|
|
|
|
const string CorsPolicy = "food-market-cors";
|
|
builder.Services.AddCors(opts => opts.AddPolicy(CorsPolicy, p =>
|
|
{
|
|
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
|
|
?? ["http://localhost:5173"];
|
|
p.WithOrigins(origins).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
|
|
}));
|
|
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddScoped<ITenantContext, HttpContextTenantContext>();
|
|
|
|
builder.Services.AddDbContext<AppDbContext>(opts =>
|
|
{
|
|
opts.UseNpgsql(builder.Configuration.GetConnectionString("Default"),
|
|
npg => npg.MigrationsAssembly(typeof(AppDbContext).Assembly.GetName().Name));
|
|
opts.UseOpenIddict();
|
|
});
|
|
|
|
builder.Services.AddIdentity<User, Role>(opts =>
|
|
{
|
|
opts.Password.RequireDigit = true;
|
|
opts.Password.RequiredLength = 8;
|
|
opts.Password.RequireNonAlphanumeric = false;
|
|
opts.Password.RequireUppercase = false;
|
|
opts.User.RequireUniqueEmail = true;
|
|
})
|
|
.AddEntityFrameworkStores<AppDbContext>()
|
|
.AddDefaultTokenProviders();
|
|
|
|
builder.Services.Configure<IdentityOptions>(opts =>
|
|
{
|
|
opts.ClaimsIdentity.UserNameClaimType = Claims.Name;
|
|
opts.ClaimsIdentity.UserIdClaimType = Claims.Subject;
|
|
opts.ClaimsIdentity.RoleClaimType = Claims.Role;
|
|
});
|
|
|
|
builder.Services.AddOpenIddict()
|
|
.AddCore(opts => opts.UseEntityFrameworkCore().UseDbContext<AppDbContext>())
|
|
.AddServer(opts =>
|
|
{
|
|
opts.SetTokenEndpointUris("/connect/token");
|
|
opts.AllowPasswordFlow().AllowRefreshTokenFlow();
|
|
opts.AcceptAnonymousClients();
|
|
opts.RegisterScopes(Scopes.OpenId, Scopes.Profile, Scopes.Email, Scopes.Roles, "api");
|
|
|
|
if (builder.Environment.IsDevelopment())
|
|
{
|
|
opts.AddEphemeralEncryptionKey().AddEphemeralSigningKey();
|
|
opts.DisableAccessTokenEncryption();
|
|
}
|
|
|
|
opts.UseAspNetCore()
|
|
.EnableTokenEndpointPassthrough()
|
|
.DisableTransportSecurityRequirement();
|
|
|
|
opts.SetAccessTokenLifetime(TimeSpan.Parse(
|
|
builder.Configuration["OpenIddict:AccessTokenLifetime"] ?? "01:00:00"));
|
|
opts.SetRefreshTokenLifetime(TimeSpan.Parse(
|
|
builder.Configuration["OpenIddict:RefreshTokenLifetime"] ?? "30.00:00:00"));
|
|
})
|
|
.AddValidation(opts =>
|
|
{
|
|
opts.UseLocalServer();
|
|
opts.UseAspNetCore();
|
|
});
|
|
|
|
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
|
builder.Services.AddAuthorization();
|
|
|
|
builder.Services.AddControllers();
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
|
|
builder.Services.AddHostedService<OpenIddictClientSeeder>();
|
|
builder.Services.AddHostedService<SystemReferenceSeeder>();
|
|
builder.Services.AddHostedService<DevDataSeeder>();
|
|
builder.Services.AddHostedService<DemoCatalogSeeder>();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseSerilogRequestLogging();
|
|
app.UseCors(CorsPolicy);
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
app.MapControllers();
|
|
|
|
app.MapGet("/health", () => Results.Ok(new { status = "ok", time = DateTime.UtcNow }));
|
|
|
|
app.MapGet("/api/me", (HttpContext ctx) =>
|
|
{
|
|
var user = ctx.User;
|
|
return Results.Ok(new
|
|
{
|
|
sub = user.FindFirst(Claims.Subject)?.Value,
|
|
name = user.Identity?.Name,
|
|
email = user.FindFirst(Claims.Email)?.Value,
|
|
roles = user.FindAll(Claims.Role).Select(c => c.Value),
|
|
orgId = user.FindFirst(HttpContextTenantContext.OrganizationClaim)?.Value,
|
|
});
|
|
}).RequireAuthorization();
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
db.Database.Migrate();
|
|
}
|
|
|
|
app.Run();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Fatal(ex, "Application terminated unexpectedly");
|
|
}
|
|
finally
|
|
{
|
|
Log.CloseAndFlush();
|
|
}
|