Merge branch 'kub-stage' into production

This commit is contained in:
masoodafar-web
2026-03-15 03:21:44 +03:30
10 changed files with 326 additions and 25 deletions
+4
View File
@@ -138,7 +138,11 @@
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) -->
<ItemGroup>
<<<<<<< HEAD
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.191" />
=======
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.192" />
>>>>>>> kub-stage
</ItemGroup>
<!-- ============================================================ -->
@@ -1,4 +1,5 @@
using BackOffice.Common.Utilities;
using System.IdentityModel.Tokens.Jwt;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
namespace BackOffice.Common.Utilities;
@@ -19,9 +20,41 @@ public class AppTokenProvider : ITokenProvider
{
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
if (!string.IsNullOrEmpty(authorizationToken))
_token = authorizationToken.ToString().Replace("Bearer ", "");
{
var raw = authorizationToken.ToString().Replace("Bearer ", "");
// Don't send expired tokens — they cause 401 on anonymous endpoints like login
if (IsTokenExpired(raw))
{
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
_token = null;
return _token;
}
_token = raw;
}
}
else if (IsTokenExpired(_token))
{
// Cached token has expired since last check
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
_token = null;
}
return _token;
}
private static bool IsTokenExpired(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
// Add 30-second buffer to avoid edge-case race
return jwt.ValidTo < DateTime.UtcNow.AddSeconds(-30);
}
catch
{
// Malformed token — treat as expired
return true;
}
}
}
+9 -9
View File
@@ -25,14 +25,14 @@ RUN rm -rf /usr/share/nginx/html/*
# Copy published wwwroot (Blazor WASM output)
COPY --from=build /app/publish/wwwroot .
# Configure nginx for SPA routing
RUN echo 'server { \
listen 80; \
server_name _; \
location / { \
root /usr/share/nginx/html; \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
# Copy entrypoint script that sets blazor-environment header
COPY ["BackOffice/docker-entrypoint.sh", "/docker-entrypoint.sh"]
RUN chmod +x /docker-entrypoint.sh
# Default Blazor environment (override via K8s env var BLAZOR_ENVIRONMENT)
ENV BLAZOR_ENVIRONMENT=Staging
# Default gateway URL for gRPC calls (injected into appsettings at startup)
ENV GW_URL=https://cms.se.kbs1.ir
EXPOSE 80
CMD ["/docker-entrypoint.sh"]
+3
View File
@@ -2,8 +2,11 @@
<configuration>
<packageSources>
<clear />
<<<<<<< HEAD
<!-- Local packages (dev builds) -->
<!-- <add key="Local" value="/home/masoud/Apps/project/FourSat/nupkg" /> -->
=======
>>>>>>> kub-stage
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
</packageSources>
@@ -1,4 +1,5 @@
@using CMSMicroservice.Protobuf.Protos.ManualPayment
@using CMSMicroservice.Protobuf.Protos.Package
@using BackOffice.Pages.Payment.Components
@using BackOffice.Pages.AutoComplete
@using Microsoft.AspNetCore.Components.Forms
@@ -13,15 +14,28 @@
<UserAutoComplete Label="انتخاب کاربر"
@bind-SelectedUserId="_selectedUserId" />
<MudNumericField T="long"
Label="مبلغ"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="تومان"
Required="true"
RequiredError="مبلغ الزامی است"
Min="1"
@bind-Value="_createModel.Amount" />
<MudSelect T="long?"
Label="انتخاب پکیج"
Value="@_selectedPackageId"
ValueChanged="OnPackageChanged"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Required="true"
RequiredError="انتخاب پکیج الزامی است">
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">
@pkg.Title — @pkg.Price.ToString("N0") تومان
</MudSelectItem>
}
</MudSelect>
@if (_selectedPackagePrice.HasValue)
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
مبلغ پکیج: <strong>@_selectedPackagePrice.Value.ToString("N0") تومان</strong> — این مبلغ به کیف‌پول کاربر اضافه می‌شود.
</MudAlert>
}
<MudSelect T="int"
Label="نوع پرداخت"
@@ -1,4 +1,5 @@
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using CMSMicroservice.Protobuf.Protos.Package;
using Google.Protobuf;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
@@ -24,19 +25,23 @@ public partial class ManualPaymentDialog
{
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public ManualPaymentContract.ManualPaymentContractClient ManualPaymentClient { get; set; } = default!;
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; } = default!;
// Snackbar is injected via _Imports.razor
[Parameter] public ManualPaymentDialogMode Mode { get; set; }
[Parameter] public ManualPaymentModel? Model { get; set; }
private ManualPaymentModel _createModel = new(){
Amount = 0,
};
private ManualPaymentModel _createModel = new(){ Amount = 0 };
private string? _adminNote;
// User selection
private long? _selectedUserId;
// Package selection
private long? _selectedPackageId;
private long? _selectedPackagePrice;
private List<GetAllPackageByFilterResponseModel> _packages = new();
// Type and Description
private int _selectedType = 1; // Default: CashDeposit
private string _description = "عضویت دستی باشگاه مشتریان";
@@ -48,6 +53,46 @@ public partial class ManualPaymentDialog
private bool _isSubmitting;
protected override async Task OnInitializedAsync()
{
await LoadPackages();
}
private async Task LoadPackages()
{
try
{
var request = new GetAllPackageByFilterRequest
{
PaginationState = new() { PageNumber = 1, PageSize = 100 },
Filter = new()
};
var result = await PackageClient.GetAllPackageByFilterAsync(request);
_packages = result.Models?.OrderBy(p => p.SortOrder).ToList()
?? new List<GetAllPackageByFilterResponseModel>();
// اگر پکیج پایه وجود دارد، پیش‌فرض انتخاب کن
var basePackage = _packages.FirstOrDefault(p => p.IsBasePackage);
if (basePackage != null)
{
_selectedPackageId = basePackage.Id;
_selectedPackagePrice = basePackage.Price;
}
}
catch
{
_packages = new List<GetAllPackageByFilterResponseModel>();
}
}
private void OnPackageChanged(long? packageId)
{
_selectedPackageId = packageId;
var pkg = _packages.FirstOrDefault(p => p.Id == packageId);
_selectedPackagePrice = pkg?.Price;
}
private async Task OnImageFileSelected(IBrowserFile? file)
{
if (file == null)
@@ -81,6 +126,12 @@ public partial class ManualPaymentDialog
return;
}
if (!_selectedPackageId.HasValue || _selectedPackageId.Value <= 0)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
if (_selectedType <= 0)
{
Snackbar.Add("لطفاً نوع پرداخت را انتخاب کنید.", Severity.Warning);
@@ -101,7 +152,8 @@ public partial class ManualPaymentDialog
var request = new CreateManualPaymentRequest
{
UserId = _selectedUserId.Value,
Amount = _createModel.Amount,
Amount = 0, // deprecated — مبلغ از پکیج خوانده می‌شود
PackageId = _selectedPackageId.Value,
Type = (ManualPaymentType)_selectedType,
Description = _description
};
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
# Generate nginx config with blazor-environment header
# BLAZOR_ENVIRONMENT defaults to "Staging" if not set
BLAZOR_ENV="${BLAZOR_ENVIRONMENT:-Staging}"
cat > /etc/nginx/conf.d/default.conf <<NGINX_CONF
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
try_files \$uri \$uri/ /index.html;
add_header blazor-environment $BLAZOR_ENV;
}
}
NGINX_CONF
# Inject GW_URL into all appsettings JSON files
# This overrides GwUrl regardless of which environment Blazor detects,
# which is critical because the service worker cache strips the
# blazor-environment header and Blazor falls back to Production.
WWWROOT="/usr/share/nginx/html"
if [ -n "$GW_URL" ]; then
echo "[entrypoint] Setting GwUrl to $GW_URL in all appsettings files"
for f in "$WWWROOT"/appsettings*.json; do
[ -f "$f" ] && sed -i "s|\"GwUrl\":\s*\"[^\"]*\"|\"GwUrl\": \"$GW_URL\"|g" "$f"
done
fi
exec nginx -g 'daemon off;'
@@ -9,7 +9,7 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/, /^appsettings.*\.json$/ ];
async function onInstall(event) {
console.info('Service worker: Install');