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');