Implement Chromium-based PDF generation service; add fetchAndDownloadPdf utility and update contract generation endpoint

This commit is contained in:
masoodafar-web
2025-11-14 15:20:46 +03:30
parent 230ba41113
commit 680ef3a7e2
9 changed files with 297 additions and 97 deletions

View File

@@ -38,8 +38,8 @@ public static class ConfigureServices
services.AddScoped<AuthDialogService>();
// Device detection: very light, dependency-free
services.AddScoped<IDeviceDetector, DeviceDetector>();
// PDF generation
services.AddSingleton<FrontOffice.Main.Utilities.Pdf.IHtmlToPdfService, FrontOffice.Main.Utilities.Pdf.HtmlToPdfService>();
// PDF generation (Chromium only)
services.AddSingleton<FrontOffice.Main.Utilities.Pdf.IChromiumPdfService, FrontOffice.Main.Utilities.Pdf.ChromiumPdfService>();
return services;
}

View File

@@ -19,14 +19,12 @@
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" />
<!-- HTML to PDF for .NET Core using PdfSharpCore -->
<PackageReference Include="PdfSharpCore" Version="1.3.56" />
<PackageReference Include="HtmlRendererCore.PdfSharpCore" Version="1.0.1" />
<!-- Keep only PuppeteerSharp for Chromium-based PDF generation -->
<PackageReference Include="PuppeteerSharp" Version="11.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Components" />
<Folder Include="Utilities/Pdf" />
</ItemGroup>
</Project>

View File

@@ -112,12 +112,12 @@
OnClick="DownloadContract">
دانلود/پرینت قرارداد نمونه
</MudButton>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: PDF از طریق Print</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: PDF</MudText>
</MudStack>
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success"
For="@(() => _model.AcceptTerms)"
Label="قوانین و مققرات را مطالعه کرده‌ام و می‌پذیرم" />
Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm>
</MudStep>
</ChildContent>

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using DateTimeConverterCL;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
@@ -19,8 +20,8 @@ public partial class RegisterWizard
private int _activeStep;
private bool _isSubmitting;
private bool _completed;
private AuthDialog _authDialog;
private MudStepper _mudStepper;
private AuthDialog? _authDialog;
private MudStepper? _mudStepper;
private UpdateUserRequest _updateUserRequest = new();
private bool _isAuthenticated;
@@ -49,17 +50,20 @@ public partial class RegisterWizard
try
{
var existUser = await UserContract.GetUserAsync(new Empty());
if (existUser != null && !string.IsNullOrEmpty(existUser.FirstName) && string.IsNullOrWhiteSpace(_model.FirstName))
if (existUser != null && !string.IsNullOrEmpty(existUser.FirstName) &&
string.IsNullOrWhiteSpace(_model.FirstName))
{
_model.FirstName = existUser.FirstName;
}
if (existUser != null && !string.IsNullOrEmpty(existUser.LastName) && string.IsNullOrWhiteSpace(_model.LastName))
if (existUser != null && !string.IsNullOrEmpty(existUser.LastName) &&
string.IsNullOrWhiteSpace(_model.LastName))
{
_model.LastName = existUser.LastName;
}
if (existUser != null && !string.IsNullOrEmpty(existUser.NationalCode) && string.IsNullOrWhiteSpace(_model.NationalCode))
if (existUser != null && !string.IsNullOrEmpty(existUser.NationalCode) &&
string.IsNullOrWhiteSpace(_model.NationalCode))
{
_model.NationalCode = existUser.NationalCode;
}
@@ -68,6 +72,7 @@ public partial class RegisterWizard
{
Console.WriteLine(e);
}
await InvokeAsync(StateHasChanged);
}
@@ -152,17 +157,64 @@ public partial class RegisterWizard
private async void DownloadContract()
{
// Example dynamic HTML (could be constructed based on user data)
var html = $"<h1 style='text-align:center'>قرارداد اولیه کاربر</h1><p>نام: {_model.FirstName ?? "-"} {_model.LastName ?? "-"}</p><p>کد ملی: {_model.NationalCode ?? "-"}</p><p>این یک نسخه نمونه است.</p>";
//var html = $"<!DOCTYPE html><html><body><h1 style='text-align:center'>قرارداد اولیه کاربر</h1><p>نام: {_model.FirstName ?? "-"} {_model.LastName ?? "-"}</p><p>کد ملی: {_model.NationalCode ?? "-"}</p><p>این یک نسخه نمونه است.</p></body></html>";
var html =
$"<!DOCTYPE html><html lang='fa' dir='rtl'><head>" +
"<meta charset='UTF-8' /><title>قرارداد تعهد و سلب مسئولیت کارابازار</title><style>" +
"body { " +
" font-family: sans-serif; " +
" direction: rtl;text-align: justify; " +
" font-size: 13px;line-height: 1.8; " +
" margin: 40px;}h1, h2 {text-align: center;margin-bottom: 10px;}" +
"h1 {font-size: 18px;}h2 {font-size: 15px;}" +
".section-title {font-weight: bold;margin-top: 15px;margin-bottom: 5px;}" +
".contract-info, .sign-section {margin-top: 20px;}.field-label {font-weight: bold;}" +
".box {border: 1px solid #000;padding: 8px;margin-top: 10px;}.small-text {font-size: 11px;line-height: 1.7;}" +
"</style></head><body><h1>قرارداد تعهد و سلب مسئولیت</h1><h2>بین کارابازار و کاربر</h2><div class='contract-info'>" +
"<p>این قرارداد تعهد و سلب مسئولیت (&laquo;قرارداد&raquo;) در تاریخ " +
$"<span class='field-label'>{DateTime.Now.MiladiToJalali()} </span>بین:</p><p>" +
"1. شرکت/مجموعه <span class='field-label'>کارابازار</span> (که از این پس &laquo;کارابازار&raquo; نامیده می‌شود)،</p><p>2. شخص حقیقی زیر (که از این پس &laquo;کاربر&raquo; نامیده می‌شود):</p><div class='box'>" +
$"<p><span class='field-label'>نام و نام خانوادگی کاربر:</span> {_model.FirstName } {_model.LastName}</p>" +
$"<p><span class='field-label'>شماره ملی:</span> {_model.NationalCode?.PersianToEnglish()}</p>" +
$"<p><span class='field-label'>شماره تماس:</span> {_model.NationalCode?.PersianToEnglish() }</p></div>" +
"<p>کارابازار و کاربر که در این قرارداد جداگانه &laquo;طرف&raquo; و مجموعاً &laquo;طرفین&raquo; نامیده می‌شوند، با علم و آگاهی کامل، مفاد زیر را پذیرفته و امضا می‌نمایند.</p></div><div><p class='section-title'>ماده ۱ - موضوع قرارداد</p>" +
"<p>موضوع این قرارداد، تعیین حدود و چارچوب تعهدات و سلب مسئولیت کارابازار در قبال استفاده کاربر ازخدمات، باشگاه مشتریان، مزایا، طرح‌های همکاری، شبکه معرفی، خرید از فروشگاه‌های طرف‌قراردادو هرگونه امکانات و تسهیلات مرتبط است که حسب مورد توسط کارابازار یا از طریق پلتفرم‌ها و شرکت‌هایهمکار در اختیار کاربر قرار می‌گیرد، بدون اینکه جزئیات مدل کسب‌وکار، شیوه تأمین منابع مالی،شبکه همکاری یا ساختار داخلی آن در این قرارداد ذکر یا افشا شود.</p>" +
"<p class='section-title'>ماده ۲ - اقرار و آگاهی کاربر</p><p>۲-۱. کاربر اقرار می‌نماید که کلیه اطلاعات کلی لازم در خصوص نحوه فعالیت، مزایا، ریسک‌های احتمالی," +
"سازوکار کلی استفاده از خدمات، و چارچوب کلی همکاری را از مراجع رسمی معرفی‌شده توسط کارابازاردریافت کرده، سؤالات خود را مطرح نموده و پاسخ مناسب را دریافت نموده است.</p><p>۲-۲. کاربر با امضای این قرارداد اقرار می‌نماید که:</p><ul><li>با اراده آزاد و بدون هرگونه اجبار، اکراه، فریب یا سوءاستفاده از ناآگاهی وارد این همکاری شده است.</li><li>از احتمال وجود ریسک‌های مالی، اعتباری، قانونی و شبکه‌ای مرتبط با فعالیت خود آگاه بوده و آن را می‌پذیرد.</li>" +
"<li>هرگونه تصمیم برای استفاده از تسهیلات، مزایا، خدمات باشگاه مشتریان، فعالیت در شبکه معرفی، خرید از فروشگاه‌ها و جذب افراد جدید، تصمیمی شخصی و آگاهانه است و به‌عنوان تعهد یا ضمانت بازده مشخص از سوی کارابازار تلقی نمی‌شود.</li></ul>" +
"<p class='section-title'>ماده ۳ - سلب مسئولیت کارابازار</p><p>۳-۱. کاربر صراحتاً می‌پذیرد که کارابازار صرفاً در چارچوب قوانین و مقررات حاکم و مطابق قراردادها و تفاهم‌نامه‌های خود با شرکای تجاری و پلتفرم‌های همکار، خدمات و امکانات مشخصی را در اختیار کاربر قرار می‌دهد و هیچ‌گونه تعهدی نسبت به:</p><ul><li>تحقق سود یا بازده مالی مشخص،</li><li>ثبات ارزش هرگونه اعتبار، امتیاز، یا منافع غیرنقدی که ممکن است در قالب طرح‌ها به کاربر تخصیص یابد،</li>" +
"<li>عملکرد اشخاص ثالث (از جمله پلتفرم‌ها، فروشگاه‌ها، شرکت‌های بیمه، ارائه‌دهندگان خدمات سلامت و هر شخص حقیقی یا حقوقی همکار)،</li><li>تصمیمات و اقدامات کاربر در جذب افراد جدید و ایجاد زیرمجموعه،</li></ul><p>نداشته و نخواهد داشت مگر در حدودی که به‌صراحت در قرارداد یا قوانین آمره بر عهده کارابازار گذاشته شده است.</p><p>" +
"۳-۲. هرگونه خسارت، دعوی، تهدید، مطالبه، ادعا، شکایت یا پیگیری حقوقی که ناشی از تصمیمات شخصی کاربر،نحوه استفاده وی از تسهیلات و مزایا، جذب دیگران، خرید کالا و خدمات، یا تعاملات او با اشخاص ثالث باشد،مستقیماً متوجه خود کاربر است و کارابازار نسبت به آن هیچ‌گونه مسئولیت مدنی، کیفری، مالی یا قراردادی ندارد," +
"مگر در مواردی که طبق قوانین آمره، مسئولیت صریح و غیرقابل‌اسقاط بر عهده کارابازار قرار گرفته باشد.</p><p>۳-۳. کاربر می‌پذیرد که هیچ‌گونه ادعایی مبنی بر فریب، اغفال، ارائه اطلاعات نادرست یا وعده‌های خارج ازچهارچوب رسمی از سوی کارابازار یا نمایندگان رسمی آن، پس از امضای این قرارداد، از وی مسموع نخواهد بود،مگر آنکه طبق رأی قطعی مرجع قضایی صالح، خلاف آن ثابت شود.</p><p class='section-title'>ماده ۴ - مسئولیت کاربر در قبال تهدید، ادعا و شکایت</p>" +
"<p>۴-۱. کاربر متعهد است در صورت طرح هرگونه تهدید، ادعا، مطالبه، شکایت یا پیگیری حقوقی از سوی اشخاص ثالث(از جمله افراد جذب‌شده توسط کاربر، زیرمجموعه‌ها، خریداران معرفی‌شده یا سایر اشخاص)، که ناشی از عملکردشخصی کاربر، معرفی‌های وی، شیوه تبلیغ، وعده‌های خارج از چارچوب رسمی کارابازار، یا نقض قوانین و مقرراتباشد، خود رأساً پاسخگو باشد و کارابازار را از هرگونه مسئولیت و هزینه احتمالی در این خصوص مصون بدارد.</p><p>۴-۲. کاربر می‌پذیرد که:</p><ul><li>هرگونه تهدید یا ادعای بی‌جا، غیرمنطقی، غیرمستند یا مغایر با مفاد این قرارداد، متوجه شخص کاربر است.</li>" +
"<li>در صورت وارد آمدن خسارت مادی یا معنوی به کارابازار ناشی از چنین تهدیدات یا ادعاهایی، کارابازار حق دارد نسبت به مطالبه خسارت از کاربر، طبق قوانین و از طریق مراجع صالح اقدام نماید.</li></ul>" +
"<p class='section-title'>ماده ۵ - محرمانگی و عدم افشای جزئیات کسب‌وکار</p><p>۵-۱. کاربر می‌پذیرد که جزئیات فنی، مالی، ساختار شبکه، شیوه توزیع مزایا، قراردادهای داخلی کارابازار باشرکای تجاری و پلتفرم‌های همکار، اطلاعات محرمانه تجاری محسوب شده و خارج از این قرارداد است و افشای آن‌هاتوسط کاربر بدون مجوز کتبی کارابازار، مجاز نمی‌باشد.</p><p>۵-۲. کاربر حق استناد به عدم درج جزئیات ساختار کسب‌وکار در این قرارداد را به‌عنوان دلیل بطلان، فسخ یاادعای فریب علیه کارابازار از خود سلب می‌نماید.</p>" +
"<p class='section-title'>ماده ۶ - کد تأیید یکبارمصرف (OTP) و شناسه قرارداد</p><p>۶-۱. به منظور تأیید هویت کاربر و جایگزین کردن امضا در محیط غیرحضوری، برای این قرارداد یک شناسه یکتا(GUID) تولید شده و از طریق سامانه کارابازار، کد تأیید یکبارمصرف (OTP) مرتبط با آن برای کاربر ارسالمی‌گردد.</p>" +
"<p>۶-۲. کاربر با وارد کردن صحیح کد تأیید (OTP) در سامانه، اقرار می‌نماید که:</p><ul><li>مالک شماره تماس ثبت‌شده در این قرارداد است یا با اجازه قانونی مالک از آن استفاده می‌کند،</li><li>از محتوای قرارداد به‌طور کامل آگاه است و آن را بدون ابهام و با رضایت کامل می‌پذیرد،</li><li>شناسه یکتا و OTP ارسال‌شده، معادل امضای الکترونیکی معتبر وی بر روی این قرارداد است.</li></ul>" +
"<p class='section-title'>ماده ۷ - مدت، اصلاح و حاکمیت قانون</p>" +
"<p>۷-۱. این قرارداد از تاریخ تأیید نهایی (ثبت OTP) نافذ و لازم‌الاجرا بوده و تا زمانی که همکاری کاربر باکارابازار ادامه دارد، معتبر خواهد بود؛ مگر آنکه مطابق قوانین یا توافق طرفین، خاتمه یابد.</p><p>۷-۲. هرگونه اصلاح در مفاد این قرارداد صرفاً از طریق اطلاع‌رسانی رسمی کارابازار و پذیرش کاربر (اعم ازامضای مکتوب، تأیید در سامانه، یا سایر روش‌های الکترونیکی مورد تأیید کارابازار) معتبر خواهد بود.</p><p>۷-۳. این قرارداد از هر حیث، تابع قوانین و مقررات حاکم بر جمهوری اسلامی ایران بوده و در صورت بروز اختلاف،مراجع صالح قضایی محل استقرار کارابازار، صلاحیت رسیدگی خواهند داشت؛ مگر آنکه به نحو دیگری بین طرفین توافقشود." +
"</p><p class='section-title'>ماده ۸ - اقرار نهایی کاربر</p><p>کاربر با امضای این قرارداد (از طریق ثبت کد تأیید یکبارمصرف OTP متناظر با شناسه یکتا)، صریحاً اعلام می‌نماید که:" +
"</p><ul><li>کلیه مفاد قرارداد را به‌دقت مطالعه نموده و آن را درک کرده است،</li>" +
"<li>پرسش‌های خود را مطرح نموده و پاسخ روشن دریافت کرده است،</li><li>ریسک‌ها، محدودیت‌ها و حدود مسئولیت کارابازار را می‌پذیرد،</li><li>حق هرگونه ادعای مغایر با مفاد این قرارداد، به‌ویژه ادعاهای بی‌اساس، غیرمنطقی یا ناشی از برداشت نادرست شخصی را از خود سلب و اسقاط می‌نماید؛ مگر در حدودی که قانون به‌طور آمره اجازه سلب آن را نداده باشد.</li></ul></div><div class='sign-section'></div><p class='small-text'></p><div class='box'><p><span class='field-label'>شناسه یکتای قرارداد :</span></p><p><span id='contract-guid'>"+Guid.NewGuid()+"</span></p></div></body></html>";
// Option A: trigger browser print dialog for PDF (client-side)
try
{
await Js.InvokeVoidAsync("printUtils.printHtml", html, new { title = "قرارداد نمونه" });
// If HTML is short use GET to simplify (URL length safe)
if (html.Length < 1800)
{
var encoded = Uri.EscapeDataString(html);
Navigation.NavigateTo($"contract/generate?html={encoded}&fileName=sample-contract", true);
return;
}
// For larger HTML use POST via JS fetch to stream file
var fileName = "sample-contract";
await Js.InvokeVoidAsync("fetchAndDownloadPdf", html, fileName);
}
catch
catch (Exception ex)
{
// Fallback to server-side generation endpoint if JS fails
Console.WriteLine($"PDF download error: {ex.Message}");
// Fallback to GET (may truncate if too long)
var encoded = Uri.EscapeDataString(html);
Navigation.NavigateTo($"contract/generate?html={encoded}&fileName=sample-contract", true);
}
@@ -170,9 +222,9 @@ public partial class RegisterWizard
private void OnPhoneVerified()
{
// Move to next step after phone verification success
// _activeStep = 1;
//StateHasChanged();
// Move to next step after phone verification success
// _activeStep = 1;
//StateHasChanged();
}
private async Task<bool> SavePersonalInfo()
@@ -185,12 +237,12 @@ public partial class RegisterWizard
try
{
// _updateUserRequest.AvatarPath="test";
// _updateUserRequest.BirthDate=Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc));
// _updateUserRequest.EmailNotifications = true;
// _updateUserRequest.PushNotifications = true;
// _updateUserRequest.SmsNotifications = true;
//
// _updateUserRequest.AvatarPath="test";
// _updateUserRequest.BirthDate=Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc));
// _updateUserRequest.EmailNotifications = true;
// _updateUserRequest.PushNotifications = true;
// _updateUserRequest.SmsNotifications = true;
//
_updateUserRequest.FirstName = _model.FirstName;
_updateUserRequest.LastName = _model.LastName;
_updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish();

View File

@@ -35,7 +35,31 @@
<!-- Load MudBlazor JS before Blazor to avoid early JS interop calls failing; add cache-busting -->
<script src="_content/MudBlazor/MudBlazor.min.js" asp-append-version="true"></script>
<script src="_framework/blazor.server.js"></script>
<script src="/js/print-utils.js" asp-append-version="true"></script>
<script>
window.fetchAndDownloadPdf = async function(html, fileName){
try {
const res = await fetch('/contract/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: html, fileName: fileName })
});
if(!res.ok){
throw new Error('PDF generation failed: ' + res.status);
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (fileName || 'contract') + '.pdf';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(()=>URL.revokeObjectURL(url), 5000);
} catch(e){
console.error(e);
}
};
</script>
<script>
// elementId: id نوار (مثلاً "top")
// containerSelector: کانتینری که اسکرول می‌خوره؛ برای MudLayout معمولا ".mud-main-content"

View File

@@ -1,6 +1,7 @@
using FluentValidation;
using FrontOffice.Main.Utilities;
using System.Net;
using FrontOffice.Main.Utilities.Pdf;
var builder = WebApplication.CreateBuilder(args);
@@ -36,41 +37,46 @@ else
builder.Services.Configure<EncryptionSettings>(builder.Configuration.GetSection("EncryptionSettings"));
builder.Services.AddSingleton<MobileNumberEncryptor>();
var app = builder.Build();
var webApp = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
if (!webApp.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
webApp.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
webApp.UseHsts();
}
app.UseHttpsRedirection();
webApp.UseHttpsRedirection();
app.UseStaticFiles();
webApp.UseStaticFiles();
app.UseRouting();
webApp.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
webApp.MapBlazorHub();
webApp.MapFallbackToPage("/_Host");
app.MapGet("/contract/sample", (FrontOffice.Main.Utilities.Pdf.IHtmlToPdfService pdfService) =>
{
var html = @"<h2 style='text-align:center'>قرارداد نمونه همکاری</h2><p>این یک متن نمونه برای قرارداد است که فقط جهت تست دانلود PDF قرار داده شده است.</p><ul><li>بند ۱: استفاده صرفا مجاز.</li><li>بند ۲: رعایت قوانین لازم است.</li><li>بند ۳: مسئولیت اطلاعات با کاربر است.</li></ul><p class='small'>تاریخ تولید: " + DateTime.Now.ToString("yyyy/MM/dd HH:mm") + "</p>";
var bytes = pdfService.GeneratePdf(html, "sample-contract");
return Results.File(bytes, "application/pdf", "sample-contract.pdf");
});
app.MapGet("/contract/generate", (string html, string? fileName, FrontOffice.Main.Utilities.Pdf.IHtmlToPdfService pdfService) =>
webApp.MapGet("/contract/generate",async (string html, string? fileName, IChromiumPdfService pdfService) =>
{
var decoded = WebUtility.UrlDecode(html);
var safeName = string.IsNullOrWhiteSpace(fileName) ? "contract" : fileName.Trim();
var bytes = pdfService.GeneratePdf(decoded, safeName);
var bytes =await pdfService.GeneratePdfAsync(decoded, safeName);
return Results.File(bytes, "application/pdf", safeName + ".pdf");
});
app.Run();
// Support large HTML via POST (body) to avoid URL length limits
webApp.MapPost("/contract/generate", async (ContractPdfRequest req, IChromiumPdfService pdfService) =>
{
if (string.IsNullOrWhiteSpace(req.Html)) return Results.BadRequest("Empty html");
var safeName = string.IsNullOrWhiteSpace(req.FileName) ? "contract" : req.FileName.Trim();
var bytes = await pdfService.GeneratePdfAsync(req.Html, safeName);
return Results.File(bytes, "application/pdf", safeName + ".pdf");
});
webApp.Run();
public sealed record ContractPdfRequest(string Html, string? FileName);
public class AppSettings

View File

@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using System.Security.Cryptography;
using System.Security.Cryptography;
using System.Text;
namespace FrontOffice.Main.Utilities;
@@ -10,21 +9,28 @@ public class MobileNumberEncryptor
private readonly string _iv;
public MobileNumberEncryptor(IConfiguration configuration)
{
var encryptionSettings = configuration.GetSection("EncryptionSettings").Get<EncryptionSettings>();
var encryptionSettings = configuration
.GetSection("EncryptionSettings")
.Get<EncryptionSettings>()
?? throw new InvalidOperationException("EncryptionSettings section not found in configuration");
_key = encryptionSettings?.Key ?? throw new ArgumentNullException("Encryption Key not found in configuration");
_iv = encryptionSettings?.IV ?? throw new ArgumentNullException("Encryption IV not found in configuration");
_key = !string.IsNullOrWhiteSpace(encryptionSettings.Key)
? encryptionSettings.Key
: throw new InvalidOperationException("Encryption Key not found in configuration");
_iv = !string.IsNullOrWhiteSpace(encryptionSettings.IV)
? encryptionSettings.IV
: throw new InvalidOperationException("Encryption IV not found in configuration");
// اعتبارسنجی سایز
ValidateKeyAndIV();
ValidateKeyAndIv();
}
public MobileNumberEncryptor(string key, string iv)
{
_key = key;
_iv = iv;
ValidateKeyAndIV();
ValidateKeyAndIv();
}
private void ValidateKeyAndIV()
private void ValidateKeyAndIv()
{
try
{
@@ -32,10 +38,10 @@ public class MobileNumberEncryptor
byte[] ivBytes = Convert.FromBase64String(_iv);
if (keyBytes.Length != 32)
throw new ArgumentException("Key must be 32 bytes in Base64 format");
throw new ArgumentException("Key must be 32 bytes in Base64 format", nameof(_key));
if (ivBytes.Length != 16)
throw new ArgumentException("IV must be 16 bytes in Base64 format");
throw new ArgumentException("IV must be 16 bytes in Base64 format", nameof(_iv));
}
catch (FormatException)
{
@@ -46,7 +52,7 @@ public class MobileNumberEncryptor
public string EncryptMobileNumber(string mobileNumber)
{
if (string.IsNullOrEmpty(mobileNumber))
throw new ArgumentException("Mobile number cannot be null or empty");
throw new ArgumentException("Mobile number cannot be null or empty", nameof(mobileNumber));
try
{
@@ -58,7 +64,7 @@ public class MobileNumberEncryptor
{
aesAlg.Key = key;
aesAlg.IV = iv;
aesAlg.Mode = CipherMode.ECB;
aesAlg.Mode = CipherMode.CBC; // ECB ignores IV; CBC is standard with IV
aesAlg.Padding = PaddingMode.PKCS7;
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
@@ -85,7 +91,7 @@ public class MobileNumberEncryptor
public string DecryptMobileNumber(string encryptedMobileNumber)
{
if (string.IsNullOrEmpty(encryptedMobileNumber))
throw new ArgumentException("Encrypted mobile number cannot be null or empty");
throw new ArgumentException("Encrypted mobile number cannot be null or empty", nameof(encryptedMobileNumber));
try
{
@@ -97,7 +103,7 @@ public class MobileNumberEncryptor
{
aesAlg.Key = key;
aesAlg.IV = iv;
aesAlg.Mode = CipherMode.ECB;
aesAlg.Mode = CipherMode.CBC;
aesAlg.Padding = PaddingMode.PKCS7;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

View File

@@ -0,0 +1,148 @@
using PuppeteerSharp;
using PuppeteerSharp.Media;
using Microsoft.Extensions.Configuration;
namespace FrontOffice.Main.Utilities.Pdf;
public interface IChromiumPdfService
{
Task<byte[]> GeneratePdfAsync(string html, string? fileName = null, CancellationToken ct = default);
}
public class ChromiumPdfService : IChromiumPdfService
{
private static Task _browserPreparationTask = Task.CompletedTask;
private static bool _browserReady;
private readonly IConfiguration _config;
private static string? _executablePath; // path to system Chrome/Chromium if available
private static IBrowser? _browser; // reused headless browser instance
private static readonly SemaphoreSlim BrowserLock = new(1, 1);
public ChromiumPdfService(IConfiguration config)
{
_config = config;
PrepareBrowserAsync();
}
private void PrepareBrowserAsync()
{
if (_browserReady) return;
_browserPreparationTask = PrepareBrowserCoreAsync();
}
private Task PrepareBrowserCoreAsync()
{
try
{
// Config / env override
_executablePath = _config["Pdf:ChromiumExecutablePath"] ?? Environment.GetEnvironmentVariable("CHROMIUM_EXECUTABLE_PATH");
if (!string.IsNullOrWhiteSpace(_executablePath) && File.Exists(_executablePath))
{
#if DEBUG
Console.WriteLine($"[PDF] Using configured Chromium executable: {_executablePath}");
#endif
_browserReady = true; return Task.CompletedTask;
}
// Common system locations
var candidates = new[]{"/usr/bin/google-chrome","/usr/bin/google-chrome-stable","/usr/bin/chromium","/usr/bin/chromium-browser","/snap/bin/chromium"};
_executablePath = candidates.FirstOrDefault(File.Exists);
if (_executablePath != null)
{
#if DEBUG
Console.WriteLine($"[PDF] Found system Chrome/Chromium: {_executablePath}");
#endif
_browserReady = true; return Task.CompletedTask;
}
// Fall back to PuppeteerSharp auto-download on first launch (no explicit path).
#if DEBUG
Console.WriteLine("[PDF] No local Chromium path resolved. Will rely on PuppeteerSharp auto-download at first launch.");
#endif
_browserReady = true; // allow launch to proceed; it will fetch if needed
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"[PDF] Unexpected preparation error: {ex.Message}");
#endif
}
return Task.CompletedTask;
}
private async Task EnsureBrowserAsync()
{
await _browserPreparationTask;
if (!_browserReady)
{
throw new InvalidOperationException("Chromium not ready for launch.");
}
if (_browser != null && !_browser.IsClosed) return;
await BrowserLock.WaitAsync();
try
{
if (_browser != null && !_browser.IsClosed) return; // double-check after lock
var launchOptions = new LaunchOptions
{
Headless = true,
Args = new[] {"--no-sandbox","--disable-setuid-sandbox"},
ExecutablePath = _executablePath // may be null; PuppeteerSharp will download if needed
};
_browser = await Puppeteer.LaunchAsync(launchOptions);
#if DEBUG
Console.WriteLine("[PDF] Chromium browser instance launched.");
#endif
}
finally
{
BrowserLock.Release();
}
}
public async Task<byte[]> GeneratePdfAsync(string html, string? fileName = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(html)) throw new ArgumentException("Html content is empty", nameof(html));
// Ensure browser available (download may happen here if not done yet)
await EnsureBrowserAsync();
ct.ThrowIfCancellationRequested();
// Inject default styles (RTL + font-face) if not present
if (!html.Contains("<html", StringComparison.OrdinalIgnoreCase))
{
html = $"<html><head><meta charset=\"utf-8\"><style>{GetDefaultStyles()}</style></head><body>{html}</body></html>";
}
else if (!html.Contains("<style", StringComparison.OrdinalIgnoreCase))
{
html = html.Replace("<head>", "<head><style>" + GetDefaultStyles() + "</style>");
}
if (_browser == null) throw new InvalidOperationException("Browser not initialized");
await using var page = await _browser.NewPageAsync();
await page.SetContentAsync(html, new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
var pdfOptions = new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
DisplayHeaderFooter = false,
MarginOptions = new MarginOptions { Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" }
};
ct.ThrowIfCancellationRequested();
var data = await page.PdfDataAsync(pdfOptions);
return data;
}
private string GetDefaultStyles()
{
// Relative paths work when using SetContent; ensure fonts are served or embedded
return "@font-face{font-family:'Vazir';src:url('fonts/Vazir-Regular.ttf') format('truetype');font-weight:400;}" +
"@font-face{font-family:'Vazir';src:url('fonts/Vazir-Medium.ttf') format('truetype');font-weight:500;}" +
"@font-face{font-family:'Vazir';src:url('fonts/Vazir-Bold.ttf') format('truetype');font-weight:700;}" +
"body{font-family:'Vazir','Arial';direction:rtl;text-align:right;font-size:12px;line-height:1.6;margin:0;padding:25px;}";
}
}

View File

@@ -1,36 +1,2 @@
using PdfSharpCore.Pdf;
//using HtmlRendererCore.PdfSharpCore;
// (removed) legacy HtmlToPdfService no longer used
namespace FrontOffice.Main.Utilities.Pdf;
public interface IHtmlToPdfService
{
byte[] GeneratePdf(string html, string? fileName = null);
}
public class HtmlToPdfService : IHtmlToPdfService
{
public byte[] GeneratePdf(string html, string? fileName = null)
{
if (string.IsNullOrWhiteSpace(html))
throw new ArgumentException("Html content is empty", nameof(html));
if (!html.Contains("<html", StringComparison.OrdinalIgnoreCase))
{
html = $"<html><head><meta charset='utf-8'><style>{GetDefaultStyles()}</style></head><body>{html}</body></html>";
}
else if (!html.Contains("<style", StringComparison.OrdinalIgnoreCase))
{
// Inject default styles if none provided
html = html.Replace("<head>", "<head><style>" + GetDefaultStyles() + "</style>");
}
using var document = new PdfDocument();
//PdfGenerator.AddPdfPages(document, html, PdfSharpCore.PageSize.A4);
using var ms = new MemoryStream();
document.Save(ms);
return ms.ToArray();
}
private static string GetDefaultStyles() => @"body{font-family:'Vazir','Arial';direction:rtl;text-align:right;font-size:12px;line-height:1.6;margin:25px;} h1,h2,h3{margin:0 0 12px;} ul{margin:0 0 12px 0;padding:0 18px;} .small{font-size:10px;color:#555;}";
}