4 Commits

Author SHA1 Message Date
masoodafar-web 62a247f097 merge: kub-stage → production (inventory auto-init + initializer worker)
Build and Deploy to Production / build-and-deploy (push) Successful in 9m2s
2026-02-18 01:01:15 +03:30
masoodafar-web bc710fdb7b feat: auto-create inventory records for new products + migration worker
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m52s
- CreateNewProductsCommandHandler: inject IInventoryService, auto-init
  inventory record with qty=0 when a new regular product is created
  (DiscountProduct handler already had this)
- InventoryInitializerService: one-time BackgroundService that runs on
  startup, finds existing products without InventoryItem records, and
  creates them (migration for legacy products)
- Register InventoryInitializerService in DI
2026-02-18 00:42:41 +03:30
masoodafar-web fd24dcebcd feat: Add SitePageSettings - simplified page management system
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m2s
- New entities: SitePageSettings + SitePageImage (4 fixed pages)
- EF Migration: AddSitePageSettings (SitePageSettings + SitePageImages tables)
- Proto: sitepagesettings.proto with 5 RPCs
- CQRS: GetPageSettings, GetAllPageSettings, SavePageSettings, SavePageImage, DeletePageImage
- gRPC Service: SitePageSettingsService (auto-registered)
- Proto NuGet bumped to 0.0.180
2026-02-17 22:34:29 +03:30
masoodafar-web 89e146bf32 fix: set Kestrel protocol to Http1AndHttp2 in appsettings — prevents 502
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m38s
2026-02-17 02:55:50 +03:30
22 changed files with 6039 additions and 6 deletions
@@ -80,6 +80,10 @@ public interface IApplicationDbContext
DbSet<SitePage> SitePages { get; }
DbSet<SitePageSection> SitePageSections { get; }
// ============= Site Page Settings (Simplified) =============
DbSet<SitePageSettings> SitePageSettingsEntities { get; }
DbSet<SitePageImage> SitePageImages { get; }
/// <summary>
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
/// </summary>
@@ -0,0 +1,36 @@
using CMSMicroservice.Application.Common.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.DeletePageImage;
// ── Command ──
public class DeletePageImageCommand : IRequest<Unit>
{
public long Id { get; set; }
}
// ── Handler ──
public class DeletePageImageCommandHandler : IRequestHandler<DeletePageImageCommand, Unit>
{
private readonly IApplicationDbContext _context;
public DeletePageImageCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeletePageImageCommand request, CancellationToken cancellationToken)
{
var entity = await _context.SitePageImages
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken);
if (entity != null)
{
entity.IsDeleted = true; // Soft delete
await _context.SaveChangesAsync(cancellationToken);
}
return Unit.Value;
}
}
@@ -0,0 +1,96 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Content;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageImage;
// ── Command ──
public class SavePageImageCommand : IRequest<SavePageImageResult>
{
public long Id { get; set; } // 0 = ایجاد جدید
public long SitePageSettingsId { get; set; }
public string ImageGroup { get; set; } = default!;
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? Description { get; set; }
public string? LinkUrl { get; set; }
public string? IconName { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
// Image upload
public byte[]? ImageFileBytes { get; set; }
public string? ImageFileMime { get; set; }
public string? ImageFileName { get; set; }
}
public class SavePageImageResult
{
public long Id { get; set; }
public bool Success { get; set; }
}
// ── Handler ──
public class SavePageImageCommandHandler : IRequestHandler<SavePageImageCommand, SavePageImageResult>
{
private readonly IApplicationDbContext _context;
private readonly IFileManager _fileManager;
public SavePageImageCommandHandler(IApplicationDbContext context, IFileManager fileManager)
{
_context = context;
_fileManager = fileManager;
}
public async Task<SavePageImageResult> Handle(SavePageImageCommand request, CancellationToken cancellationToken)
{
SitePageImage entity;
if (request.Id > 0)
{
// ویرایش
entity = await _context.SitePageImages
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
?? throw new Exception($"تصویر با شناسه {request.Id} یافت نشد");
}
else
{
// ایجاد جدید
entity = new SitePageImage
{
SitePageSettingsId = request.SitePageSettingsId,
ImageGroup = request.ImageGroup,
ImagePath = string.Empty // placeholder — will be set below
};
_context.SitePageImages.Add(entity);
}
entity.Title = request.Title;
entity.Subtitle = request.Subtitle;
entity.Description = request.Description;
entity.LinkUrl = request.LinkUrl;
entity.IconName = request.IconName;
entity.SortOrder = request.SortOrder;
entity.IsActive = request.IsActive;
// آپلود تصویر
if (request.ImageFileBytes is { Length: > 0 })
{
var result = await _fileManager.UploadImageAsync(
$"Images/SitePageSettings/{request.ImageGroup}",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
entity.ImagePath = result.Main.Path;
entity.ThumbnailPath = result.Thumbnail.Path;
}
await _context.SaveChangesAsync(cancellationToken);
return new SavePageImageResult { Id = entity.Id, Success = true };
}
}
@@ -0,0 +1,83 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Content;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
// ── Command ──
public class SavePageSettingsCommand : IRequest<SavePageSettingsResult>
{
public string PageKey { get; set; } = default!;
public string Title { get; set; } = default!;
public string? MetaDescription { get; set; }
public string? HeroTitle { get; set; }
public string? HeroSubtitle { get; set; }
public bool IsActive { get; set; }
public string? SettingsJson { get; set; }
// Hero image upload
public byte[]? ImageFileBytes { get; set; }
public string? ImageFileMime { get; set; }
public string? ImageFileName { get; set; }
}
public class SavePageSettingsResult
{
public long Id { get; set; }
public bool Success { get; set; }
}
// ── Handler ──
public class SavePageSettingsCommandHandler : IRequestHandler<SavePageSettingsCommand, SavePageSettingsResult>
{
private readonly IApplicationDbContext _context;
private readonly IFileManager _fileManager;
public SavePageSettingsCommandHandler(IApplicationDbContext context, IFileManager fileManager)
{
_context = context;
_fileManager = fileManager;
}
public async Task<SavePageSettingsResult> Handle(SavePageSettingsCommand request, CancellationToken cancellationToken)
{
var entity = await _context.SitePageSettingsEntities
.FirstOrDefaultAsync(x => x.PageKey == request.PageKey && !x.IsDeleted, cancellationToken);
if (entity == null)
{
// ایجاد صفحه جدید (فقط برای seed اولیه — صفحات ثابت هستند)
entity = new SitePageSettings
{
PageKey = request.PageKey,
};
_context.SitePageSettingsEntities.Add(entity);
}
entity.Title = request.Title;
entity.MetaDescription = request.MetaDescription;
entity.HeroTitle = request.HeroTitle;
entity.HeroSubtitle = request.HeroSubtitle;
entity.IsActive = request.IsActive;
entity.SettingsJson = request.SettingsJson;
// آپلود تصویر هیرو
if (request.ImageFileBytes is { Length: > 0 })
{
var result = await _fileManager.UploadImageAsync(
"Images/SitePageSettings",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
entity.HeroImagePath = result.Main.Path;
}
await _context.SaveChangesAsync(cancellationToken);
return new SavePageSettingsResult { Id = entity.Id, Success = true };
}
}
@@ -0,0 +1,30 @@
using FluentValidation;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
public class SavePageSettingsCommandValidator : AbstractValidator<SavePageSettingsCommand>
{
private static readonly string[] ValidPageKeys = { "landing", "about", "contact", "licenses" };
public SavePageSettingsCommandValidator()
{
RuleFor(x => x.PageKey)
.NotEmpty().WithMessage("کلید صفحه الزامی است")
.MaximumLength(50)
.Must(key => ValidPageKeys.Contains(key))
.WithMessage("کلید صفحه باید یکی از مقادیر landing, about, contact, licenses باشد");
RuleFor(x => x.Title)
.NotEmpty().WithMessage("عنوان صفحه الزامی است")
.MaximumLength(200).WithMessage("عنوان صفحه حداکثر ۲۰۰ کاراکتر");
RuleFor(x => x.MetaDescription)
.MaximumLength(300).WithMessage("توضیحات متا حداکثر ۳۰۰ کاراکتر");
RuleFor(x => x.HeroTitle)
.MaximumLength(200).WithMessage("عنوان هیرو حداکثر ۲۰۰ کاراکتر");
RuleFor(x => x.HeroSubtitle)
.MaximumLength(500).WithMessage("زیرعنوان هیرو حداکثر ۵۰۰ کاراکتر");
}
}
@@ -0,0 +1,49 @@
using CMSMicroservice.Application.Common.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetAllPageSettings;
// ── Query ──
public class GetAllPageSettingsQuery : IRequest<List<PageSettingsSummaryDto>>
{
}
// ── Handler ──
public class GetAllPageSettingsQueryHandler : IRequestHandler<GetAllPageSettingsQuery, List<PageSettingsSummaryDto>>
{
private readonly IApplicationDbContext _context;
public GetAllPageSettingsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<List<PageSettingsSummaryDto>> Handle(GetAllPageSettingsQuery request, CancellationToken cancellationToken)
{
return await _context.SitePageSettingsEntities
.Where(x => !x.IsDeleted)
.OrderBy(x => x.Id)
.Select(x => new PageSettingsSummaryDto
{
Id = x.Id,
PageKey = x.PageKey,
Title = x.Title,
IsActive = x.IsActive,
ImageCount = x.Images.Count(i => !i.IsDeleted),
LastModified = x.LastModified ?? x.Created
})
.ToListAsync(cancellationToken);
}
}
// ── DTO ──
public class PageSettingsSummaryDto
{
public long Id { get; set; }
public string PageKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public bool IsActive { get; set; }
public int ImageCount { get; set; }
public DateTime LastModified { get; set; }
}
@@ -0,0 +1,92 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Content;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetPageSettings;
// ── Query ──
public class GetPageSettingsQuery : IRequest<PageSettingsDto?>
{
public string PageKey { get; set; } = default!;
}
// ── Handler ──
public class GetPageSettingsQueryHandler : IRequestHandler<GetPageSettingsQuery, PageSettingsDto?>
{
private readonly IApplicationDbContext _context;
public GetPageSettingsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<PageSettingsDto?> Handle(GetPageSettingsQuery request, CancellationToken cancellationToken)
{
var entity = await _context.SitePageSettingsEntities
.Include(x => x.Images.Where(i => !i.IsDeleted && i.IsActive).OrderBy(i => i.SortOrder))
.FirstOrDefaultAsync(x => x.PageKey == request.PageKey && !x.IsDeleted, cancellationToken);
if (entity == null)
return null;
return MapToDto(entity);
}
public static PageSettingsDto MapToDto(SitePageSettings entity) => new()
{
Id = entity.Id,
PageKey = entity.PageKey,
Title = entity.Title,
MetaDescription = entity.MetaDescription,
HeroTitle = entity.HeroTitle,
HeroSubtitle = entity.HeroSubtitle,
HeroImagePath = entity.HeroImagePath,
IsActive = entity.IsActive,
SettingsJson = entity.SettingsJson,
Images = entity.Images.Select(i => new PageImageDto
{
Id = i.Id,
ImageGroup = i.ImageGroup,
Title = i.Title,
Subtitle = i.Subtitle,
Description = i.Description,
ImagePath = i.ImagePath,
ThumbnailPath = i.ThumbnailPath,
LinkUrl = i.LinkUrl,
IconName = i.IconName,
SortOrder = i.SortOrder,
IsActive = i.IsActive
}).ToList()
};
}
// ── DTOs ──
public class PageSettingsDto
{
public long Id { get; set; }
public string PageKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? MetaDescription { get; set; }
public string? HeroTitle { get; set; }
public string? HeroSubtitle { get; set; }
public string? HeroImagePath { get; set; }
public bool IsActive { get; set; }
public string? SettingsJson { get; set; }
public List<PageImageDto> Images { get; set; } = new();
}
public class PageImageDto
{
public long Id { get; set; }
public string ImageGroup { get; set; } = string.Empty;
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? Description { get; set; }
public string ImagePath { get; set; } = string.Empty;
public string? ThumbnailPath { get; set; }
public string? LinkUrl { get; set; }
public string? IconName { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
@@ -1,6 +1,7 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
@@ -9,15 +10,18 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
{
private readonly IApplicationDbContext _context;
private readonly IFileManager _fileManager;
private readonly IInventoryService _inventoryService;
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
public CreateNewProductsCommandHandler(
IApplicationDbContext context,
IFileManager fileManager,
IInventoryService inventoryService,
ILogger<CreateNewProductsCommandHandler> logger)
{
_context = context;
_fileManager = fileManager;
_inventoryService = inventoryService;
_logger = logger;
}
@@ -70,6 +74,20 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
await _context.Products.AddAsync(entity, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// ایجاد رکورد موجودی در سیستم انبارداری
try
{
await _inventoryService.InitializeInventoryAsync(
entity.Id,
ProductType.RegularProduct,
0, // موجودی اولیه صفر — باید از طریق Inventory اضافه شود
ct: cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to initialize inventory for Product Id={ProductId}", entity.Id);
}
// Handle category assignments
if (request.CategoryIds is { Count: > 0 })
{
@@ -0,0 +1,70 @@
using CMSMicroservice.Domain.Common;
namespace CMSMicroservice.Domain.Entities.Content;
/// <summary>
/// تصاویر مرتبط با صفحات سایت
/// شامل: مجوزها، اعضای تیم، ارزش‌ها و سایر آیتم‌های تصویری
/// </summary>
public class SitePageImage : BaseAuditableEntity
{
/// <summary>
/// شناسه صفحه مرتبط
/// </summary>
public long SitePageSettingsId { get; set; }
/// <summary>
/// گروه تصویر (licenses, team, values)
/// </summary>
public string ImageGroup { get; set; } = string.Empty;
/// <summary>
/// عنوان
/// </summary>
public string? Title { get; set; }
/// <summary>
/// زیرعنوان
/// </summary>
public string? Subtitle { get; set; }
/// <summary>
/// توضیحات
/// </summary>
public string? Description { get; set; }
/// <summary>
/// مسیر تصویر اصلی
/// </summary>
public string ImagePath { get; set; } = string.Empty;
/// <summary>
/// مسیر تامبنیل
/// </summary>
public string? ThumbnailPath { get; set; }
/// <summary>
/// لینک خارجی (برای مجوزها: لینک به سایت مرجع)
/// </summary>
public string? LinkUrl { get; set; }
/// <summary>
/// نام آیکون Material
/// </summary>
public string? IconName { get; set; }
/// <summary>
/// ترتیب نمایش
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// فعال بودن
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// صفحه مرتبط
/// </summary>
public virtual SitePageSettings SitePageSettings { get; set; } = null!;
}
@@ -0,0 +1,56 @@
using CMSMicroservice.Domain.Common;
namespace CMSMicroservice.Domain.Entities.Content;
/// <summary>
/// تنظیمات صفحات سایت — هر صفحه یک رکورد با تنظیمات JSON تایپ‌شده
/// صفحات ثابت: landing, about, contact, licenses
/// </summary>
public class SitePageSettings : BaseAuditableEntity
{
/// <summary>
/// کلید یکتای صفحه (landing, about, contact, licenses)
/// </summary>
public string PageKey { get; set; } = string.Empty;
/// <summary>
/// عنوان صفحه
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// توضیح متا برای SEO
/// </summary>
public string? MetaDescription { get; set; }
/// <summary>
/// عنوان بخش Hero
/// </summary>
public string? HeroTitle { get; set; }
/// <summary>
/// زیرعنوان بخش Hero
/// </summary>
public string? HeroSubtitle { get; set; }
/// <summary>
/// مسیر تصویر Hero
/// </summary>
public string? HeroImagePath { get; set; }
/// <summary>
/// فعال بودن صفحه
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// تنظیمات اختصاصی صفحه به فرمت JSON
/// ساختار JSON بر اساس PageKey متفاوت است
/// </summary>
public string? SettingsJson { get; set; }
/// <summary>
/// تصاویر مرتبط با صفحه (مجوزها، تیم، ارزش‌ها)
/// </summary>
public virtual ICollection<SitePageImage> Images { get; set; } = new List<SitePageImage>();
}
@@ -0,0 +1,153 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Infrastructure.BackgroundServices;
/// <summary>
/// سرویس یکباره (Migration) برای مقداردهی اولیه انبارداری از محصولات قدیمی.
/// محصولاتی که قبل از فعال‌سازی سیستم انبارداری ایجاد شده‌اند رکورد InventoryItem ندارند.
/// این Worker در استارتاپ اجرا شده، برای آنها رکورد می‌سازد و سپس متوقف می‌شود.
/// توجه: محصولات جدید از طریق CreateProductCommandHandler خودکار رکورد انبار دریافت می‌کنند.
/// این سرویس را می‌توانید بعد از اجرای اولیه از DI حذف کنید.
/// </summary>
public class InventoryInitializerService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<InventoryInitializerService> _logger;
public InventoryInitializerService(
IServiceScopeFactory scopeFactory,
ILogger<InventoryInitializerService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// صبر کوتاه برای اطمینان از آماده شدن دیتابیس
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
_logger.LogInformation("InventoryInitializerService started — scanning products for missing inventory records...");
try
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
var inventoryService = scope.ServiceProvider.GetRequiredService<IInventoryService>();
var (regularCount, discountCount) = await InitializeAllProducts(context, inventoryService, stoppingToken);
_logger.LogInformation(
"InventoryInitializerService completed — initialized {RegularCount} regular products and {DiscountCount} discount products",
regularCount, discountCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "InventoryInitializerService encountered an error");
}
// این Worker فقط یکبار اجرا می‌شود
_logger.LogInformation("InventoryInitializerService finished — shutting down (one-time execution)");
}
private async Task<(int regularCount, int discountCount)> InitializeAllProducts(
IApplicationDbContext context,
IInventoryService inventoryService,
CancellationToken ct)
{
var regularCount = 0;
var discountCount = 0;
// ── محصولات عادی ──
// محصولاتی که هنوز InventoryItem برایشان ایجاد نشده
var existingRegularProductIds = await context.InventoryItems
.Where(i => !i.IsDeleted && i.ProductId != null)
.Select(i => i.ProductId!.Value)
.ToListAsync(ct);
var regularProducts = await context.Products
.Where(p => !p.IsDeleted && !existingRegularProductIds.Contains(p.Id))
.Select(p => new { p.Id, p.Title, p.RemainingCount })
.ToListAsync(ct);
_logger.LogInformation("Found {Count} regular products without inventory records", regularProducts.Count);
foreach (var product in regularProducts)
{
if (ct.IsCancellationRequested) break;
try
{
var initialQty = Math.Max(0, product.RemainingCount);
await inventoryService.InitializeInventoryAsync(
product.Id,
ProductType.RegularProduct,
initialQty,
warehouseId: null, // انبار پیش‌فرض (ID=1)
lowStockThreshold: 10,
ct);
regularCount++;
_logger.LogDebug(
"Initialized inventory for Regular Product Id={ProductId} ({Title}), Qty={Qty}",
product.Id, product.Title, initialQty);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to initialize inventory for Regular Product Id={ProductId} ({Title})",
product.Id, product.Title);
}
}
// ── محصولات تخفیفی ──
var existingDiscountProductIds = await context.InventoryItems
.Where(i => !i.IsDeleted && i.DiscountProductId != null)
.Select(i => i.DiscountProductId!.Value)
.ToListAsync(ct);
var discountProducts = await context.DiscountProducts
.Where(p => !p.IsDeleted && !existingDiscountProductIds.Contains(p.Id))
.Select(p => new { p.Id, p.Title, p.RemainingCount })
.ToListAsync(ct);
_logger.LogInformation("Found {Count} discount products without inventory records", discountProducts.Count);
foreach (var product in discountProducts)
{
if (ct.IsCancellationRequested) break;
try
{
var initialQty = Math.Max(0, product.RemainingCount);
await inventoryService.InitializeInventoryAsync(
product.Id,
ProductType.DiscountProduct,
initialQty,
warehouseId: null,
lowStockThreshold: 10,
ct);
discountCount++;
_logger.LogDebug(
"Initialized inventory for Discount Product Id={ProductId} ({Title}), Qty={Qty}",
product.Id, product.Title, initialQty);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to initialize inventory for Discount Product Id={ProductId} ({Title})",
product.Id, product.Title);
}
}
return (regularCount, discountCount);
}
}
@@ -123,6 +123,9 @@ public static class ConfigureServices
// Expire pending discount orders after 30 minutes
services.AddHostedService<ExpirePendingOrdersService>();
// One-time: Initialize inventory records for existing products
services.AddHostedService<InventoryInitializerService>();
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>
@@ -152,4 +152,8 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
// ============= Content Management DbSets =============
public DbSet<SitePage> SitePages => Set<SitePage>();
public DbSet<SitePageSection> SitePageSections => Set<SitePageSection>();
// ============= Site Page Settings (Simplified) =============
public DbSet<SitePageSettings> SitePageSettingsEntities => Set<SitePageSettings>();
public DbSet<SitePageImage> SitePageImages => Set<SitePageImage>();
}
@@ -0,0 +1,54 @@
using CMSMicroservice.Domain.Entities.Content;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
public class SitePageImageConfiguration : IEntityTypeConfiguration<SitePageImage>
{
public void Configure(EntityTypeBuilder<SitePageImage> builder)
{
builder.ToTable("SitePageImages");
builder.HasKey(x => x.Id);
builder.Property(x => x.SitePageSettingsId)
.IsRequired();
builder.Property(x => x.ImageGroup)
.IsRequired()
.HasMaxLength(50);
builder.Property(x => x.Title)
.HasMaxLength(200);
builder.Property(x => x.Subtitle)
.HasMaxLength(300);
builder.Property(x => x.ImagePath)
.IsRequired();
builder.Property(x => x.IconName)
.HasMaxLength(100);
builder.Property(x => x.LinkUrl)
.HasMaxLength(500);
builder.Property(x => x.SortOrder)
.IsRequired()
.HasDefaultValue(0);
builder.Property(x => x.IsActive)
.IsRequired()
.HasDefaultValue(true);
// رابطه با SitePageSettings
builder.HasOne(x => x.SitePageSettings)
.WithMany(x => x.Images)
.HasForeignKey(x => x.SitePageSettingsId)
.OnDelete(DeleteBehavior.Cascade);
// ایندکس ترکیبی
builder.HasIndex(x => new { x.SitePageSettingsId, x.ImageGroup })
.HasDatabaseName("IX_SitePageImages_SettingsId_Group");
}
}
@@ -0,0 +1,44 @@
using CMSMicroservice.Domain.Entities.Content;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace CMSMicroservice.Infrastructure.Persistence.Configurations;
public class SitePageSettingsConfiguration : IEntityTypeConfiguration<SitePageSettings>
{
public void Configure(EntityTypeBuilder<SitePageSettings> builder)
{
builder.ToTable("SitePageSettings");
builder.HasKey(x => x.Id);
builder.Property(x => x.PageKey)
.IsRequired()
.HasMaxLength(50);
builder.Property(x => x.Title)
.IsRequired()
.HasMaxLength(200);
builder.Property(x => x.MetaDescription)
.HasMaxLength(300);
builder.Property(x => x.HeroTitle)
.HasMaxLength(200);
builder.Property(x => x.HeroSubtitle)
.HasMaxLength(500);
builder.Property(x => x.IsActive)
.IsRequired()
.HasDefaultValue(true);
// SettingsJson — ستون JSON بدون محدودیت طول
builder.Property(x => x.SettingsJson)
.HasColumnType("nvarchar(max)");
// ایندکس یکتا روی PageKey
builder.HasIndex(x => x.PageKey)
.IsUnique()
.HasDatabaseName("IX_SitePageSettings_PageKey");
}
}
@@ -0,0 +1,102 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddSitePageSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SitePageSettings",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
PageKey = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
MetaDescription = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
HeroTitle = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
HeroSubtitle = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
HeroImagePath = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
SettingsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SitePageSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SitePageImages",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
SitePageSettingsId = table.Column<long>(type: "bigint", nullable: false),
ImageGroup = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Subtitle = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
ImagePath = table.Column<string>(type: "nvarchar(max)", nullable: false),
ThumbnailPath = table.Column<string>(type: "nvarchar(max)", nullable: true),
LinkUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
IconName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SitePageImages", x => x.Id);
table.ForeignKey(
name: "FK_SitePageImages_SitePageSettings_SitePageSettingsId",
column: x => x.SitePageSettingsId,
principalSchema: "CMS",
principalTable: "SitePageSettings",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SitePageImages_SettingsId_Group",
schema: "CMS",
table: "SitePageImages",
columns: new[] { "SitePageSettingsId", "ImageGroup" });
migrationBuilder.CreateIndex(
name: "IX_SitePageSettings_PageKey",
schema: "CMS",
table: "SitePageSettings",
column: "PageKey",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SitePageImages",
schema: "CMS");
migrationBuilder.DropTable(
name: "SitePageSettings",
schema: "CMS");
}
}
}
@@ -826,6 +826,81 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("SitePages", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageImage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("IconName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ImageGroup")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ImagePath")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("LinkUrl")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<long>("SitePageSettingsId")
.HasColumnType("bigint");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<string>("Subtitle")
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.Property<string>("ThumbnailPath")
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("SitePageSettingsId", "ImageGroup")
.HasDatabaseName("IX_SitePageImages_SettingsId_Group");
b.ToTable("SitePageImages", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSection", b =>
{
b.Property<long>("Id")
@@ -900,6 +975,71 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("SitePageSections", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSettings", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("HeroImagePath")
.HasColumnType("nvarchar(max)");
b.Property<string>("HeroSubtitle")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("HeroTitle")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("MetaDescription")
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.Property<string>("PageKey")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("SettingsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("PageKey")
.IsUnique()
.HasDatabaseName("IX_SitePageSettings_PageKey");
b.ToTable("SitePageSettings", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
{
b.Property<long>("Id")
@@ -3807,6 +3947,17 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("WeekDefinition");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageImage", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Content.SitePageSettings", "SitePageSettings")
.WithMany("Images")
.HasForeignKey("SitePageSettingsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("SitePageSettings");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSection", b =>
{
b.HasOne("CMSMicroservice.Domain.Entities.Content.SitePage", "SitePage")
@@ -4363,6 +4514,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Navigation("Sections");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSettings", b =>
{
b.Navigation("Images");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
{
b.Navigation("UserContracts");
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.179</Version>
<Version>0.0.180</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -71,6 +71,7 @@
<Protobuf Include="Protos\blogcategory.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<Protobuf Include="Protos\blogpostimage.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<Protobuf Include="Protos\sitepage.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<Protobuf Include="Protos\sitepagesettings.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- Image Resolver Service -->
<Protobuf Include="Protos\imageresolver.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup>
@@ -0,0 +1,162 @@
syntax = "proto3";
package sitepagesettings;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.SitePageSettings";
// ══════════════════════════════════════════════════════════════
// سرویس مدیریت صفحات سایت (نسخه ساده‌شده)
// ۴ صفحه ثابت: landing, about, contact, licenses
// ══════════════════════════════════════════════════════════════
service SitePageSettingsContract
{
// دریافت تنظیمات صفحه با کلید — مورد استفاده FrontOffice
rpc GetPageSettings(GetPageSettingsRequest) returns (PageSettingsResponse){
option (google.api.http) = { get: "/api/page-settings/{page_key}" };
};
// دریافت لیست همه صفحات — مورد استفاده BackOffice
rpc GetAllPageSettings(GetAllPageSettingsRequest) returns (GetAllPageSettingsResponse){
option (google.api.http) = { get: "/api/page-settings" };
};
// ذخیره تنظیمات صفحه — مورد استفاده BackOffice
rpc SavePageSettings(SavePageSettingsRequest) returns (SavePageSettingsResponse){
option (google.api.http) = { put: "/api/page-settings" body: "*" };
};
// ذخیره تصویر صفحه (مجوزها، تیم، ارزش‌ها) — مورد استفاده BackOffice
rpc SavePageImage(SavePageImageRequest) returns (SavePageImageResponse){
option (google.api.http) = { put: "/api/page-settings/images" body: "*" };
};
// حذف تصویر صفحه — مورد استفاده BackOffice
rpc DeletePageImage(DeletePageImageRequest) returns (google.protobuf.Empty){
option (google.api.http) = { delete: "/api/page-settings/images/{id}" };
};
}
// ══════════════════════════════════════════════════════════════
// Get Page Settings
// ══════════════════════════════════════════════════════════════
message GetPageSettingsRequest
{
string page_key = 1; // "landing" | "about" | "contact" | "licenses"
}
message PageSettingsResponse
{
int64 id = 1;
string page_key = 2;
string title = 3;
google.protobuf.StringValue meta_description = 4;
google.protobuf.StringValue hero_title = 5;
google.protobuf.StringValue hero_subtitle = 6;
google.protobuf.StringValue hero_image_path = 7;
bool is_active = 8;
string settings_json = 9; // JSON تنظیمات — ساختار بر اساس page_key متفاوت
repeated PageImageItem images = 10; // تصاویر مرتبط (مجوزها، تیم، ارزش‌ها)
}
message PageImageItem
{
int64 id = 1;
string image_group = 2; // "licenses" | "team" | "values"
google.protobuf.StringValue title = 3;
google.protobuf.StringValue subtitle = 4;
google.protobuf.StringValue description = 5;
string image_path = 6;
google.protobuf.StringValue thumbnail_path = 7;
google.protobuf.StringValue link_url = 8;
google.protobuf.StringValue icon_name = 9;
int32 sort_order = 10;
bool is_active = 11;
}
// ══════════════════════════════════════════════════════════════
// Get All Page Settings (Summary)
// ══════════════════════════════════════════════════════════════
message GetAllPageSettingsRequest {}
message GetAllPageSettingsResponse
{
repeated PageSettingsSummary pages = 1;
}
message PageSettingsSummary
{
int64 id = 1;
string page_key = 2;
string title = 3;
bool is_active = 4;
int32 image_count = 5;
google.protobuf.Timestamp last_modified = 6;
}
// ══════════════════════════════════════════════════════════════
// Save Page Settings
// ══════════════════════════════════════════════════════════════
message SavePageSettingsRequest
{
string page_key = 1; // کلید صفحه (نمیتونه تغییر کنه)
string title = 2;
google.protobuf.StringValue meta_description = 3;
google.protobuf.StringValue hero_title = 4;
google.protobuf.StringValue hero_subtitle = 5;
bool is_active = 6;
string settings_json = 7; // JSON تنظیمات
PageSettingsImageFile hero_image_file = 8; // آپلود تصویر Hero (اختیاری)
}
message SavePageSettingsResponse
{
int64 id = 1;
bool success = 2;
}
// ══════════════════════════════════════════════════════════════
// Save Page Image (Add/Update)
// ══════════════════════════════════════════════════════════════
message SavePageImageRequest
{
int64 id = 1; // 0 = ایجاد جدید، > 0 = ویرایش
int64 site_page_settings_id = 2; // شناسه صفحه
string image_group = 3; // "licenses" | "team" | "values"
google.protobuf.StringValue title = 4;
google.protobuf.StringValue subtitle = 5;
google.protobuf.StringValue description = 6;
google.protobuf.StringValue link_url = 7;
google.protobuf.StringValue icon_name = 8;
int32 sort_order = 9;
bool is_active = 10;
PageSettingsImageFile image_file = 11; // فایل تصویر (اختیاری برای ویرایش)
}
message SavePageImageResponse
{
int64 id = 1;
bool success = 2;
}
// ══════════════════════════════════════════════════════════════
// Delete Page Image
// ══════════════════════════════════════════════════════════════
message DeletePageImageRequest
{
int64 id = 1;
}
// ══════════════════════════════════════════════════════════════
// File Upload Model
// ══════════════════════════════════════════════════════════════
message PageSettingsImageFile
{
bytes file = 1;
string mime = 2;
string file_name = 3;
}
@@ -0,0 +1,146 @@
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
using CMSMicroservice.Application.SitePageSettingsCQ.Commands.DeletePageImage;
using CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageImage;
using CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
using CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetAllPageSettings;
using CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetPageSettings;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class SitePageSettingsService : SitePageSettingsContract.SitePageSettingsContractBase
{
private readonly ISender _sender;
public SitePageSettingsService(ISender sender)
{
_sender = sender;
}
// ── GetPageSettings ──
public override async Task<PageSettingsResponse> GetPageSettings(GetPageSettingsRequest request, ServerCallContext context)
{
var query = new GetPageSettingsQuery { PageKey = request.PageKey };
var result = await _sender.Send(query, context.CancellationToken);
if (result == null)
throw new RpcException(new Status(StatusCode.NotFound, $"صفحه با کلید '{request.PageKey}' یافت نشد"));
return MapToResponse(result);
}
// ── GetAllPageSettings ──
public override async Task<GetAllPageSettingsResponse> GetAllPageSettings(GetAllPageSettingsRequest request, ServerCallContext context)
{
var query = new GetAllPageSettingsQuery();
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllPageSettingsResponse();
foreach (var item in result)
{
response.Pages.Add(new PageSettingsSummary
{
Id = item.Id,
PageKey = item.PageKey,
Title = item.Title,
IsActive = item.IsActive,
ImageCount = item.ImageCount,
LastModified = Timestamp.FromDateTime(DateTime.SpecifyKind(item.LastModified, DateTimeKind.Utc))
});
}
return response;
}
// ── SavePageSettings ──
public override async Task<SavePageSettingsResponse> SavePageSettings(SavePageSettingsRequest request, ServerCallContext context)
{
var command = new SavePageSettingsCommand
{
PageKey = request.PageKey,
Title = request.Title,
MetaDescription = request.MetaDescription,
HeroTitle = request.HeroTitle,
HeroSubtitle = request.HeroSubtitle,
IsActive = request.IsActive,
SettingsJson = request.SettingsJson,
ImageFileBytes = request.HeroImageFile?.File?.ToByteArray(),
ImageFileMime = request.HeroImageFile?.Mime,
ImageFileName = request.HeroImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new SavePageSettingsResponse { Id = result.Id, Success = result.Success };
}
// ── SavePageImage ──
public override async Task<SavePageImageResponse> SavePageImage(SavePageImageRequest request, ServerCallContext context)
{
var command = new SavePageImageCommand
{
Id = request.Id,
SitePageSettingsId = request.SitePageSettingsId,
ImageGroup = request.ImageGroup,
Title = request.Title,
Subtitle = request.Subtitle,
Description = request.Description,
LinkUrl = request.LinkUrl,
IconName = request.IconName,
SortOrder = request.SortOrder,
IsActive = request.IsActive,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new SavePageImageResponse { Id = result.Id, Success = result.Success };
}
// ── DeletePageImage ──
public override async Task<Empty> DeletePageImage(DeletePageImageRequest request, ServerCallContext context)
{
var command = new DeletePageImageCommand { Id = request.Id };
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
// ── Private Mapping Helpers ──
private static PageSettingsResponse MapToResponse(PageSettingsDto dto)
{
var response = new PageSettingsResponse
{
Id = dto.Id,
PageKey = dto.PageKey,
Title = dto.Title,
MetaDescription = dto.MetaDescription,
HeroTitle = dto.HeroTitle,
HeroSubtitle = dto.HeroSubtitle,
HeroImagePath = dto.HeroImagePath,
IsActive = dto.IsActive,
SettingsJson = dto.SettingsJson ?? string.Empty
};
foreach (var img in dto.Images)
{
response.Images.Add(new PageImageItem
{
Id = img.Id,
ImageGroup = img.ImageGroup,
Title = img.Title,
Subtitle = img.Subtitle,
Description = img.Description,
ImagePath = img.ImagePath,
ThumbnailPath = img.ThumbnailPath,
LinkUrl = img.LinkUrl,
IconName = img.IconName,
SortOrder = img.SortOrder,
IsActive = img.IsActive
});
}
return response;
}
}
+5 -5
View File
@@ -13,6 +13,11 @@
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 5,
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http1AndHttp2"
}
},
"ConnectionStrings": {
"DefaultConnection": "Data Source=45.149.79.127,31433; Initial Catalog=KBS;User ID=sa;Password=YourStrong@Passw0rd;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
"providerName": "System.Data.SqlClient"
@@ -71,11 +76,6 @@
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
},
"Authentication": {
"Authority": "https://ids.domain.com/",
"Audience": "domain_api"