diff --git a/.gitea/workflows/kub-deploy.yml b/.gitea/workflows/kub-deploy.yml
new file mode 100644
index 0000000..8e355fe
--- /dev/null
+++ b/.gitea/workflows/kub-deploy.yml
@@ -0,0 +1,81 @@
+name: Build and Deploy to Kubernetes
+
+on:
+ push:
+ branches:
+ - kub-stage
+
+env:
+ REGISTRY: 194.5.195.53:30080
+ IMAGE_NAME: admin/backoffice
+ K8S_SERVER: 194.5.195.53
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ container:
+ image: 194.5.195.53:32082/docker-sshpass:latest
+ options: --privileged
+ steps:
+ - name: Start Docker daemon
+ run: |
+ mkdir -p /etc/docker
+ cat > /etc/docker/daemon.json << 'DAEMON'
+ {
+ "insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
+ }
+ DAEMON
+ echo "🚀 Starting Docker daemon..."
+ dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
+
+ # Wait up to 3 minutes for Docker to be ready
+ for i in $(seq 1 90); do
+ if docker info >/dev/null 2>&1; then
+ echo "✅ Docker daemon is ready (attempt $i)"
+ docker version
+ break
+ else
+ echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
+ sleep 2
+ fi
+ done
+
+ # Final check
+ if ! docker info >/dev/null 2>&1; then
+ echo "❌ Docker daemon failed to start after 3 minutes"
+ exit 1
+ fi
+
+ - name: Checkout code
+ run: |
+ git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/BackOffice.git .
+
+ - name: Login to Docker registries
+ run: |
+ 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 Docker Image
+ run: |
+ cd src
+ DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f BackOffice/Dockerfile .
+
+ - name: Push to Registry
+ run: |
+ docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - 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 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
diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj
index b13cf35..92208ca 100644
--- a/src/BackOffice/BackOffice.csproj
+++ b/src/BackOffice/BackOffice.csproj
@@ -138,7 +138,11 @@
+<<<<<<< HEAD
+=======
+
+>>>>>>> kub-stage
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
diff --git a/src/BackOffice/Dockerfile b/src/BackOffice/Dockerfile
index 5b08d14..2b8bac4 100644
--- a/src/BackOffice/Dockerfile
+++ b/src/BackOffice/Dockerfile
@@ -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"]
diff --git a/src/BackOffice/NuGet.config b/src/BackOffice/NuGet.config
index dea4acf..37dfa7c 100644
--- a/src/BackOffice/NuGet.config
+++ b/src/BackOffice/NuGet.config
@@ -2,8 +2,11 @@
+<<<<<<< HEAD
+=======
+>>>>>>> kub-stage
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
};
diff --git a/src/BackOffice/docker-entrypoint.sh b/src/BackOffice/docker-entrypoint.sh
new file mode 100644
index 0000000..2cf6f93
--- /dev/null
+++ b/src/BackOffice/docker-entrypoint.sh
@@ -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 < 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');