From 85208da1b50a5a662d77005808f46bb51bcea408 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Wed, 11 Mar 2026 23:44:36 +0330 Subject: [PATCH 01/11] feat: manual payment - replace amount with package selector - Remove manual amount input field - Add package selector dropdown (shows name + price) - Pre-select base package, show price in alert - Validate package selection before submit - Send package_id to CMS, proto NuGet 0.0.192 --- src/BackOffice/BackOffice.csproj | 2 +- .../Components/ManualPaymentDialog.razor | 32 +++++++--- .../Components/ManualPaymentDialog.razor.cs | 60 +++++++++++++++++-- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index 32ce305..f626858 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -138,7 +138,7 @@ - + diff --git a/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor b/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor index dd34aeb..0d357d3 100644 --- a/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor +++ b/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor @@ -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 @@ - + + @foreach (var pkg in _packages) + { + + @pkg.Title — @pkg.Price.ToString("N0") تومان + + } + + + @if (_selectedPackagePrice.HasValue) + { + + مبلغ پکیج: @_selectedPackagePrice.Value.ToString("N0") تومان — این مبلغ به کیف‌پول کاربر اضافه می‌شود. + + } _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(); + + // اگر پکیج پایه وجود دارد، پیش‌فرض انتخاب کن + var basePackage = _packages.FirstOrDefault(p => p.IsBasePackage); + if (basePackage != null) + { + _selectedPackageId = basePackage.Id; + _selectedPackagePrice = basePackage.Price; + } + } + catch + { + _packages = new List(); + } + } + + 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 }; From 7147e9930f75e314cb5672932a4d8b23ce6c0e26 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 12 Mar 2026 00:03:49 +0330 Subject: [PATCH 02/11] fix: bypass broken Nexus, use nuget.org + local proto build - NuGet.config: nuget.org for standard packages, local nupkg for proto - Dockerfile: COPY nupkg/ before restore - CI: build proto from CMS source before Docker build --- .gitea/workflows/kub-deploy.yml | 15 +++++++++++++++ src/BackOffice/Dockerfile | 3 ++- src/BackOffice/NuGet.config | 11 +++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/kub-deploy.yml b/.gitea/workflows/kub-deploy.yml index 9e15309..b45256f 100644 --- a/.gitea/workflows/kub-deploy.yml +++ b/.gitea/workflows/kub-deploy.yml @@ -55,6 +55,21 @@ jobs: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin + - name: Build Proto Package + run: | + echo "📦 Building Proto package from CMS..." + git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git /tmp/cms + docker run --rm --network host \ + -v /tmp/cms:/cms \ + -v $(pwd)/src/nupkg:/out \ + 194.5.195.53:32082/dotnet/sdk:9.0 sh -c ' + cd /cms/src/CMSMicroservice.Protobuf + dotnet restore --source https://api.nuget.org/v3/index.json + dotnet build -c Release --no-restore + dotnet pack -c Release --no-build -o /out + ' + echo "✅ Proto package: $(ls src/nupkg/)" + - name: Build Docker Image run: | cd src diff --git a/src/BackOffice/Dockerfile b/src/BackOffice/Dockerfile index 5b08d14..a0c6b4b 100644 --- a/src/BackOffice/Dockerfile +++ b/src/BackOffice/Dockerfile @@ -1,8 +1,9 @@ FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build WORKDIR /src -# Copy NuGet config and project file +# Copy NuGet config, local packages, and project file COPY ["BackOffice/NuGet.config", "NuGet.config"] +COPY ["nupkg/", "nupkg/"] COPY ["BackOffice/BackOffice.csproj", "BackOffice/"] # Restore dependencies diff --git a/src/BackOffice/NuGet.config b/src/BackOffice/NuGet.config index 6402f43..474b477 100644 --- a/src/BackOffice/NuGet.config +++ b/src/BackOffice/NuGet.config @@ -2,15 +2,10 @@ + - - + + - - - - - - From aa1d21a616d21c186bfdefb4dc286fbd9eef14a7 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 12 Mar 2026 00:06:24 +0330 Subject: [PATCH 03/11] Revert "fix: bypass broken Nexus, use nuget.org + local proto build" This reverts commit 7147e9930f75e314cb5672932a4d8b23ce6c0e26. --- .gitea/workflows/kub-deploy.yml | 15 --------------- src/BackOffice/Dockerfile | 3 +-- src/BackOffice/NuGet.config | 11 ++++++++--- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/.gitea/workflows/kub-deploy.yml b/.gitea/workflows/kub-deploy.yml index b45256f..9e15309 100644 --- a/.gitea/workflows/kub-deploy.yml +++ b/.gitea/workflows/kub-deploy.yml @@ -55,21 +55,6 @@ jobs: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin - - name: Build Proto Package - run: | - echo "📦 Building Proto package from CMS..." - git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/CMS.git /tmp/cms - docker run --rm --network host \ - -v /tmp/cms:/cms \ - -v $(pwd)/src/nupkg:/out \ - 194.5.195.53:32082/dotnet/sdk:9.0 sh -c ' - cd /cms/src/CMSMicroservice.Protobuf - dotnet restore --source https://api.nuget.org/v3/index.json - dotnet build -c Release --no-restore - dotnet pack -c Release --no-build -o /out - ' - echo "✅ Proto package: $(ls src/nupkg/)" - - name: Build Docker Image run: | cd src diff --git a/src/BackOffice/Dockerfile b/src/BackOffice/Dockerfile index a0c6b4b..5b08d14 100644 --- a/src/BackOffice/Dockerfile +++ b/src/BackOffice/Dockerfile @@ -1,9 +1,8 @@ FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build WORKDIR /src -# Copy NuGet config, local packages, and project file +# Copy NuGet config and project file COPY ["BackOffice/NuGet.config", "NuGet.config"] -COPY ["nupkg/", "nupkg/"] COPY ["BackOffice/BackOffice.csproj", "BackOffice/"] # Restore dependencies diff --git a/src/BackOffice/NuGet.config b/src/BackOffice/NuGet.config index 474b477..6402f43 100644 --- a/src/BackOffice/NuGet.config +++ b/src/BackOffice/NuGet.config @@ -2,10 +2,15 @@ - - - + + + + + + + + From 851a1bbc4f9ccca5158ce79bbf0d5e9ac4592610 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 12 Mar 2026 01:14:19 +0330 Subject: [PATCH 04/11] ci: retry build - all NuGet packages pushed to Nexus From 148a9e0493c27b1e0cb5a9cd3caa9df2eb7ba78a Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 12 Mar 2026 02:28:07 +0330 Subject: [PATCH 05/11] fix: remove local nupkg source from NuGet.config (breaks Docker builds) --- src/BackOffice/NuGet.config | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/BackOffice/NuGet.config b/src/BackOffice/NuGet.config index 6402f43..c53f59f 100644 --- a/src/BackOffice/NuGet.config +++ b/src/BackOffice/NuGet.config @@ -2,8 +2,6 @@ - - From 76dbee7d413722ff3e54a9b8480ba05bb9598061 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Thu, 12 Mar 2026 07:01:46 +0330 Subject: [PATCH 06/11] ci: retry - pushed missing 9.0.12 SDK packages to Nexus From 1ef5edf9391423db51e889fb418a15bfc724e621 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 15 Mar 2026 00:19:04 +0330 Subject: [PATCH 07/11] fix: add blazor-environment header to nginx for staging config BackOffice WASM app defaulted to Production environment because nginx didn't set the blazor-environment response header. This caused the app to load appsettings.Production.json (GwUrl=https://cms.kbs1.ir) instead of appsettings.Staging.json (GwUrl=https://cms.se.kbs1.ir), resulting in 401 gRPC errors on the login page. - Add docker-entrypoint.sh that generates nginx config with blazor-environment header - Default BLAZOR_ENVIRONMENT=Staging (configurable via K8s env var) - This ensures the WASM app loads the correct staging configuration --- src/BackOffice/Dockerfile | 16 +++++++--------- src/BackOffice/docker-entrypoint.sh | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 src/BackOffice/docker-entrypoint.sh diff --git a/src/BackOffice/Dockerfile b/src/BackOffice/Dockerfile index 5b08d14..a89ba28 100644 --- a/src/BackOffice/Dockerfile +++ b/src/BackOffice/Dockerfile @@ -25,14 +25,12 @@ 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 EXPOSE 80 +CMD ["/docker-entrypoint.sh"] diff --git a/src/BackOffice/docker-entrypoint.sh b/src/BackOffice/docker-entrypoint.sh new file mode 100644 index 0000000..b0ceecd --- /dev/null +++ b/src/BackOffice/docker-entrypoint.sh @@ -0,0 +1,18 @@ +#!/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 < Date: Sun, 15 Mar 2026 00:42:38 +0330 Subject: [PATCH 08/11] fix: inject GwUrl into appsettings at container startup The blazor-environment header is stripped by the Blazor WASM service worker cache, causing Blazor to fall back to Production environment and load appsettings.Production.json (GwUrl=https://cms.kbs1.ir). Fix: use sed in docker-entrypoint.sh to replace GwUrl in ALL appsettings*.json files at container startup with the GW_URL env var. Default GW_URL=https://cms.se.kbs1.ir for staging deployment. --- src/BackOffice/Dockerfile | 2 ++ src/BackOffice/docker-entrypoint.sh | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/BackOffice/Dockerfile b/src/BackOffice/Dockerfile index a89ba28..2b8bac4 100644 --- a/src/BackOffice/Dockerfile +++ b/src/BackOffice/Dockerfile @@ -31,6 +31,8 @@ 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"] diff --git a/src/BackOffice/docker-entrypoint.sh b/src/BackOffice/docker-entrypoint.sh index b0ceecd..2cf6f93 100644 --- a/src/BackOffice/docker-entrypoint.sh +++ b/src/BackOffice/docker-entrypoint.sh @@ -15,4 +15,16 @@ server { } 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;' From b3e6066c90b899bc670bfaf20440ed49dccd647a Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 15 Mar 2026 01:39:27 +0330 Subject: [PATCH 09/11] fix: don't send expired JWT tokens with gRPC requests AppTokenProvider now checks token expiry before returning it. Expired tokens are removed from localStorage and not sent. This fixes 401 errors on anonymous endpoints (login/OTP) caused by the gRPC channel attaching an old expired Bearer token to every request. CMS validates the token even on unauthenticated endpoints and rejects expired ones with 401. --- .../Common/Utilities/AppTokenProvider.cs | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/BackOffice/Common/Utilities/AppTokenProvider.cs b/src/BackOffice/Common/Utilities/AppTokenProvider.cs index 6abbf8e..dc40351 100644 --- a/src/BackOffice/Common/Utilities/AppTokenProvider.cs +++ b/src/BackOffice/Common/Utilities/AppTokenProvider.cs @@ -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(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; + } + } } \ No newline at end of file From 9699833ac9d16caa10454ac3d06a836c0f71bc92 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 15 Mar 2026 03:07:14 +0330 Subject: [PATCH 10/11] fix: exclude appsettings from service worker cache appsettings*.json files were cached by the service worker, causing stale GwUrl (cms.kbs1.ir) to persist in the browser even after the server files were updated. Now appsettings files are always fetched fresh from the server, where the entrypoint injects the correct GW_URL. --- src/BackOffice/wwwroot/service-worker.published.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BackOffice/wwwroot/service-worker.published.js b/src/BackOffice/wwwroot/service-worker.published.js index 24a9f72..1df8f82 100644 --- a/src/BackOffice/wwwroot/service-worker.published.js +++ b/src/BackOffice/wwwroot/service-worker.published.js @@ -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'); From 2a7bb0592d8ddad7016471192a3abd9ce7840a51 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sun, 15 Mar 2026 03:11:38 +0330 Subject: [PATCH 11/11] feat: add K8s deployment manifest with GW_URL and BLAZOR_ENVIRONMENT - Create k8s/staging/backoffice-deployment.yaml with env vars: GW_URL=https://cms.se.kbs1.ir (injected into appsettings at startup) BLAZOR_ENVIRONMENT=Staging - Update CI/CD to apply K8s manifests on deploy (like CMS does) - This makes configuration persistent across CI/CD deployments --- .gitea/workflows/kub-deploy.yml | 11 +++- k8s/staging/backoffice-deployment.yaml | 84 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 k8s/staging/backoffice-deployment.yaml diff --git a/.gitea/workflows/kub-deploy.yml b/.gitea/workflows/kub-deploy.yml index 9e15309..8e355fe 100644 --- a/.gitea/workflows/kub-deploy.yml +++ b/.gitea/workflows/kub-deploy.yml @@ -67,8 +67,15 @@ jobs: - name: Deploy to Kubernetes run: | export SSHPASS="${{ secrets.SERVER_PASSWORD }}" + + # Copy K8s manifests to server + sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/backoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/backoffice-deployment.yaml + + # Apply manifests and restart sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} " - kubectl rollout restart deployment/backoffice - kubectl rollout status deployment/backoffice --timeout=180s + kubectl apply -f /tmp/backoffice-deployment.yaml && + kubectl rollout restart deployment/backoffice && + kubectl rollout status deployment/backoffice --timeout=180s && + rm -f /tmp/backoffice-deployment.yaml " echo "✅ Deployed!" diff --git a/k8s/staging/backoffice-deployment.yaml b/k8s/staging/backoffice-deployment.yaml new file mode 100644 index 0000000..811cbd7 --- /dev/null +++ b/k8s/staging/backoffice-deployment.yaml @@ -0,0 +1,84 @@ +--- +# BackOffice UI Deployment (Blazor WASM + nginx) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backoffice + namespace: default + labels: + app: backoffice +spec: + replicas: 1 + selector: + matchLabels: + app: backoffice + template: + metadata: + labels: + app: backoffice + spec: + containers: + - name: backoffice + image: 194.5.195.53:30080/admin/backoffice:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + name: http + env: + - name: BLAZOR_ENVIRONMENT + value: "Staging" + - name: GW_URL + value: "https://cms.se.kbs1.ir" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + imagePullSecrets: + - name: gitea-registry-secret + +--- +# BackOffice UI Service +apiVersion: v1 +kind: Service +metadata: + name: backoffice-svc + namespace: default +spec: + selector: + app: backoffice + ports: + - port: 80 + targetPort: 80 + type: ClusterIP + +--- +# BackOffice UI Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: backoffice-ingress + namespace: default + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/service-upstream: "true" +spec: + ingressClassName: nginx + tls: + - hosts: + - backoffice.se.kbs1.ir + secretName: backoffice-tls + rules: + - host: backoffice.se.kbs1.ir + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: backoffice-svc + port: + number: 80