From 89d42184a16d6c6b1076c589c19fa60a65d512f3 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 4 Jun 2026 19:09:31 +0330 Subject: [PATCH] Add Soroush CI/CD (Gitea + Nexus) + self-host fonts for offline build Pipeline (.gitea/workflows/ci-cd.yml), all images/packages via Nexus mirror: - CI api-build: dotnet restore/build server/Hokm.slnx + run Hokm.Sim (rules). - CI web-check: npm install + tsc --noEmit + next build (static export). - deploy (self-hosted): pre-deploy pg_dump backup, rollback image tag, build, bring up db -> server -> web with stop+rm+up --no-deps (no force-recreate, no bare compose down), health-wait each, prune. Local stack (docker-compose.yml), ports in 1500-1600 so it coexists with manual dev on 3000/5005: web :1500 (nginx static) -> server :1505 (.NET) -> db :1510 (postgres, named volume + backups). Dockerfiles: server (.NET, NuGet via nuget.docker.config, binds 0.0.0.0, busybox wget healthcheck) + web (Next static export -> nginx, NEXT_PUBLIC_* baked as build args). nginx.conf SPA fallback. Config: server CORS is now config-driven (Cors__Origins) so the deployed web origin is allowed without code edits. deploy/ENV_FILE.example documents the Gitea ENV_FILE secret; DEPLOY.md covers setup/run/LAN-IP/rollback/migrations. Fonts: switch Vazirmatn + Plus Jakarta Sans from next/font/google (build-time Google fetch -> fails on the Iran CI runner) to self-hosted @fontsource-variable packages. Build is offline and ~3x faster; 7 woff2 emitted into out/. Verified locally: dotnet build slnx + Hokm.Sim (300 matches, exit 0); tsc clean; next build clean with self-hosted fonts. Co-Authored-By: Claude Opus 4.8 --- .dockerignore | 15 +++ .gitea/workflows/ci-cd.yml | 185 ++++++++++++++++++++++++++++++ DEPLOY.md | 85 ++++++++++++++ Dockerfile | 22 ++++ deploy/ENV_FILE.example | 36 ++++++ docker-compose.yml | 86 ++++++++++++++ nginx.conf | 18 +++ package-lock.json | 20 ++++ package.json | 2 + server/.dockerignore | 7 ++ server/Dockerfile | 21 ++++ server/nuget.docker.config | 15 +++ server/src/Hokm.Server/Program.cs | 15 ++- src/app/globals.css | 6 + src/app/layout.tsx | 22 +--- 15 files changed, 534 insertions(+), 21 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci-cd.yml create mode 100644 DEPLOY.md create mode 100644 Dockerfile create mode 100644 deploy/ENV_FILE.example create mode 100644 docker-compose.yml create mode 100644 nginx.conf create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile create mode 100644 server/nuget.docker.config diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a011f4b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +.next +out +android +server +.git +.gitea +*.md +.env* +!.env.example +Dockerfile +.dockerignore +docker-compose*.yml +npm-debug.log* +tsconfig.tsbuildinfo diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml new file mode 100644 index 0000000..be893d5 --- /dev/null +++ b/.gitea/workflows/ci-cd.yml @@ -0,0 +1,185 @@ +name: CI/CD + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: hokm-cicd-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ---------------------------------------------------------------- API (.NET) + api-build: + name: "CI - API (dotnet build + engine sim)" + runs-on: ubuntu-latest + container: + image: mirror.soroushasadi.com/dotnet/sdk:10.0 + options: --add-host=gitea:host-gateway + steps: + - name: Checkout + env: + TOKEN: ${{ github.token }} + REF: ${{ github.ref }} + run: | + git init -q + git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" + git config http.extraheader "Authorization: Bearer ${TOKEN}" + git fetch --depth=1 origin "${REF}" + git checkout -q FETCH_HEAD + + - name: Write NuGet config + run: | + cat > /tmp/nuget.ci.config << 'EOF' + + + + + + + + + + + + EOF + + - name: Restore + run: dotnet restore server/Hokm.slnx --configfile /tmp/nuget.ci.config + env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: 1 + + - name: Build + run: dotnet build server/Hokm.slnx --no-restore -c Release + + - name: Engine simulation (rules validation) + run: dotnet run --project server/tools/Hokm.Sim/Hokm.Sim.csproj -c Release --no-build + + # ----------------------------------------------------------- Web (Next.js) + web-check: + name: "CI - Web (tsc + next build)" + runs-on: ubuntu-latest + container: + image: mirror.soroushasadi.com/node:20-alpine + options: --add-host=gitea:host-gateway + steps: + - name: Checkout (tarball) + env: + TOKEN: ${{ github.token }} + SHA: ${{ github.sha }} + run: | + wget -q --header "Authorization: Bearer ${TOKEN}" \ + "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/archive/${SHA}.tar.gz" \ + -O /tmp/repo.tar.gz + tar -xzf /tmp/repo.tar.gz --strip-components=1 + + - name: Install + run: npm install --legacy-peer-deps --ignore-scripts --registry https://mirror.soroushasadi.com/repository/npm-group/ + + - name: TypeScript check + run: npx tsc --noEmit + + - name: Build (static export) + run: npm run build + + # -------------------------------------------------------------- Deploy + deploy: + name: "Deploy - local stack (db + server + web)" + runs-on: self-hosted + needs: [api-build, web-check] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + timeout-minutes: 40 + env: + # act host runner starts with a minimal PATH — extend so docker is found. + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin + steps: + - name: Checkout + env: + TOKEN: ${{ github.token }} + REF: ${{ github.ref }} + run: | + git init -q + git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" + git config http.extraheader "Authorization: Bearer ${TOKEN}" + git fetch --depth=1 origin "${REF}" + git checkout -q FETCH_HEAD + + - name: Write .env + run: printf '%s' "$ENV_FILE" > .env + env: + ENV_FILE: ${{ secrets.ENV_FILE }} + + - name: Backup database (if running) + run: | + mkdir -p /opt/hokm-backups + if docker ps --format '{{.Names}}' | grep -q '^hokm-db$'; then + TS=$(date +%Y%m%d-%H%M%S) + docker exec hokm-db pg_dump -U hokm hokm > "/opt/hokm-backups/hokm-${TS}.sql" \ + && echo "backed up to /opt/hokm-backups/hokm-${TS}.sql" \ + || echo "WARN: pg_dump failed (continuing)" + else + echo "no hokm-db container yet — first deploy, nothing to back up" + fi + + - name: Tag rollback image + run: | + CURRENT=$(docker inspect hokm-server --format='{{.Config.Image}}' 2>/dev/null || echo "") + if [ -n "$CURRENT" ]; then docker tag "$CURRENT" hokm-server:rollback && echo "rollback tag = $CURRENT"; fi + + - name: Build images + run: docker compose build --parallel server web + env: + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 + + - name: Start database + run: docker compose up -d --no-deps db + + - name: Wait for database healthy + run: | + for i in $(seq 1 20); do + S=$(docker inspect --format='{{.State.Health.Status}}' hokm-db 2>/dev/null || echo missing) + echo " [$i/20] db: $S" + [ "$S" = "healthy" ] && break + [ "$i" = "20" ] && { echo "TIMEOUT db"; docker logs --tail=40 hokm-db; exit 1; } + sleep 3 + done + + - name: Deploy server (stop + rm + up, no force-recreate) + run: | + docker stop hokm-server 2>/dev/null || true + docker rm hokm-server 2>/dev/null || true + docker compose up -d --no-deps server + + - name: Wait for server healthy + run: | + for i in $(seq 1 24); do + S=$(docker inspect --format='{{.State.Health.Status}}' hokm-server 2>/dev/null || echo missing) + echo " [$i/24] server: $S" + [ "$S" = "healthy" ] && { echo "OK hokm-server healthy"; break; } + [ "$i" = "24" ] && { echo "TIMEOUT hokm-server"; docker compose logs --tail=60 server; exit 1; } + sleep 5 + done + + - name: Deploy web (stop + rm + up, no force-recreate) + run: | + docker stop hokm-web 2>/dev/null || true + docker rm hokm-web 2>/dev/null || true + docker compose up -d --no-deps web + + - name: Wait for web healthy + run: | + for i in $(seq 1 18); do + S=$(docker inspect --format='{{.State.Health.Status}}' hokm-web 2>/dev/null || echo missing) + echo " [$i/18] web: $S" + [ "$S" = "healthy" ] && { echo "OK hokm-web healthy"; break; } + [ "$i" = "18" ] && { echo "TIMEOUT hokm-web"; docker compose logs --tail=40 web; exit 1; } + sleep 5 + done + + - name: Prune dangling images + if: success() + run: docker image prune -f diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..ddf3f71 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,85 @@ +# Deploy — Barg-e Vasat (Soroush CI/CD) + +CI/CD runs on **Gitea Actions** (`git.soroushasadi.com`) with all packages and +base images pulled through the **Nexus mirror** (`mirror.soroushasadi.com`). +Pushing to `main` triggers build → deploy. + +## Topology + +| Service | Image | Container | Host port | Notes | +|---|---|---|---|---| +| Postgres | `postgres:16-alpine` | `hokm-db` | `1510` | named volume `hokm_db_data` | +| API (.NET SignalR) | `hokm-server:latest` | `hokm-server` | `1505` → 5005 | EF Core → Postgres | +| Web (static Next → nginx) | `hokm-web:latest` | `hokm-web` | `1500` → 80 | `NEXT_PUBLIC_*` baked at build | + +Ports are in **1500–1600** on purpose, so the deployed stack runs alongside a +manual `npm run dev` (:3000) and `dotnet run` (:5005) without colliding. + +## Pipeline (`.gitea/workflows/ci-cd.yml`) + +1. **CI – API**: restore (Nexus NuGet) → `dotnet build server/Hokm.slnx` → run `Hokm.Sim` (engine rules validation). +2. **CI – Web**: `npm install` (Nexus npm) → `tsc --noEmit` → `next build` (static export). +3. **Deploy** (`self-hosted`, push to `main` only): backup DB → tag rollback image → build images → bring up `db` (wait healthy) → `server` (stop+rm+up, wait healthy) → `web` (stop+rm+up, wait healthy) → prune. + +Deploy follows the safety rules: pre-deploy `pg_dump` backup to `/opt/hokm-backups`, +rollback tag before replace, explicit `stop + rm + up --no-deps` (no +`--force-recreate`, no bare `docker compose down`). + +## One-time setup + +1. **Secret**: fill `deploy/ENV_FILE.example` and paste into the Gitea repo secret + `ENV_FILE` at `.../HokmPlay/settings/secrets`. At minimum set `JWT_KEY` + (`openssl rand -hex 32`) and `POSTGRES_PASSWORD`. +2. **Runner**: an `act_runner` registered with both labels + (`ubuntu-latest:docker://...` for CI, `self-hosted:host` for deploy) — reused + from existing Soroush projects. +3. **Push**: `git push origin main` → watch `.../HokmPlay/actions`. + +## Reaching the stack + +- Same machine as the deploy host: open `http://localhost:1500`. +- Different machine (browser elsewhere): set `NEXT_PUBLIC_SERVER_URL` and + `CORS_ORIGINS` in `ENV_FILE` to the host **LAN IP** (e.g. + `http://172.28.144.1:1505` / `http://172.28.144.1:1500`) and push again — + the API URL is baked into the web bundle at build time. (localhost can be + hijacked by the VPN; prefer the LAN IP.) + +## Local test (no Gitea, on your machine) + +```bash +cd D:\Projects\hokm +copy deploy\ENV_FILE.example .env # then edit JWT_KEY / POSTGRES_PASSWORD +docker compose build server web +docker compose up -d +# web → http://localhost:1500 api → http://localhost:1505/ +docker compose logs -f server +``` + +Tear down (keeps the DB volume): +```bash +docker compose stop +``` + +## Migrations + +The server auto-applies EF migrations when any exist, else `EnsureCreated()` +(current state — no migration classes yet, so the Postgres schema is created on +first boot). When you generate them: + +```bash +cd server/src/Hokm.Server +$env:HOKM_DESIGN_CONN="Host=localhost;Port=1510;Database=hokm;Username=hokm;Password=" +dotnet ef migrations add Init +``` + +Then the next deploy runs `Database.Migrate()` automatically. + +## Rollback + +```bash +docker stop hokm-server && docker rm hokm-server +docker run -d --name hokm-server --network hokm_default -p 1505:5005 \ + --env-file <(grep -E '^(JWT_|Database__|ConnectionStrings__|Cors__|Zarinpal__)' .env) \ + hokm-server:rollback +``` +(or just revert the commit and push — CI redeploys the previous code.) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f2db910 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Barg-e Vasat web (Next.js 16 static export → nginx) +# The app is output:"export" (fully client-side), so we build the static `out/` +# and serve it with nginx. NEXT_PUBLIC_* are baked at build time. +FROM mirror.soroushasadi.com/node:20-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install --legacy-peer-deps --ignore-scripts \ + --registry https://mirror.soroushasadi.com/repository/npm-group/ +COPY . . +# Live mode + the API origin the BROWSER will use (host-mapped port / LAN IP). +ARG NEXT_PUBLIC_USE_SERVER=1 +ARG NEXT_PUBLIC_SERVER_URL=http://localhost:1505 +ENV NEXT_PUBLIC_USE_SERVER=$NEXT_PUBLIC_USE_SERVER +ENV NEXT_PUBLIC_SERVER_URL=$NEXT_PUBLIC_SERVER_URL +RUN npm run build + +FROM mirror.soroushasadi.com/nginx:alpine +COPY --from=build /app/out /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +HEALTHCHECK --interval=10s --timeout=5s --retries=6 --start-period=10s \ + CMD wget -q -O- http://127.0.0.1/ || exit 1 diff --git a/deploy/ENV_FILE.example b/deploy/ENV_FILE.example new file mode 100644 index 0000000..ea5b9ae --- /dev/null +++ b/deploy/ENV_FILE.example @@ -0,0 +1,36 @@ +# ────────────────────────────────────────────────────────────────────────── +# Barg-e Vasat — ENV_FILE +# Paste the contents of this file (filled in) into the Gitea repo secret: +# https://git.soroushasadi.com/soroushdes/HokmPlay/settings/secrets → ENV_FILE +# The deploy job writes it verbatim to `.env`, which docker compose reads. +# +# NOTE: NEXT_PUBLIC_SERVER_URL is baked into the web bundle at BUILD time — +# changing it requires a new CI run (push a commit) to take effect. +# ────────────────────────────────────────────────────────────────────────── + +# Host ports (1500–1600 range so the stack coexists with manual dev on 3000/5005) +WEB_PORT=1500 +API_PORT=1505 +DB_PORT=1510 + +# Database (postgres container) +POSTGRES_PASSWORD=change-me-strong-password + +# JWT — generate with: openssl rand -hex 32 +JWT_KEY=CHANGE-ME-to-a-32+char-random-secret +JWT_ISSUER=hokm +JWT_AUDIENCE=hokm-clients + +# Browser-facing API origin (host-mapped api port). +# If the browser is NOT on the deploy host, use the host LAN IP instead of +# localhost, e.g. http://172.28.144.1:1505 (localhost can be VPN-hijacked). +NEXT_PUBLIC_SERVER_URL=http://localhost:1505 + +# Origins allowed by the API's CORS (comma-separated). Must include the web URL. +CORS_ORIGINS=http://localhost:1500 + +# ZarinPal (sandbox for now — switch in admin/panel later) +ZARINPAL_MERCHANT_ID=299685fb-cadf-4dfc-98e2-d4af5d81528d +ZARINPAL_SANDBOX=true +ZARINPAL_CALLBACK_URL=http://localhost:1505/api/coins/pay/callback +ZARINPAL_CLIENT_RETURN_URL=http://localhost:1500 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bf14e49 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,86 @@ +# Barg-e Vasat — local/self-hosted stack. +# Ports live in the 1500–1600 range so this stack can run alongside a manual +# `npm run dev` (:3000) and `dotnet run` (:5005) without colliding. +# web → http://localhost:1500 +# api → http://localhost:1505 +# db → localhost:1510 (postgres) +# All values come from .env (the deploy job writes it from the ENV_FILE secret). + +services: + db: + image: mirror.soroushasadi.com/postgres:16-alpine + container_name: hokm-db + restart: unless-stopped + environment: + POSTGRES_DB: hokm + POSTGRES_USER: hokm + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-hokm_dev_pass} + volumes: + - hokm_db_data:/var/lib/postgresql/data + ports: + - "${DB_PORT:-1510}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U hokm -d hokm"] + interval: 5s + timeout: 5s + retries: 10 + + server: + build: + context: ./server + dockerfile: Dockerfile + image: hokm-server:latest + container_name: hokm-server + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Production + ASPNETCORE_URLS: http://0.0.0.0:5005 + Database__Provider: postgres + ConnectionStrings__Default: "Host=db;Port=5432;Database=hokm;Username=hokm;Password=${POSTGRES_PASSWORD:-hokm_dev_pass}" + Jwt__Key: ${JWT_KEY:?set JWT_KEY in .env} + Jwt__Issuer: ${JWT_ISSUER:-hokm} + Jwt__Audience: ${JWT_AUDIENCE:-hokm-clients} + # Comma-separated origins the browser uses to reach the web app. + Cors__Origins: ${CORS_ORIGINS:-http://localhost:1500} + Zarinpal__MerchantId: ${ZARINPAL_MERCHANT_ID:-299685fb-cadf-4dfc-98e2-d4af5d81528d} + Zarinpal__Sandbox: ${ZARINPAL_SANDBOX:-true} + Zarinpal__CallbackUrl: ${ZARINPAL_CALLBACK_URL:-http://localhost:1505/api/coins/pay/callback} + Zarinpal__ClientReturnUrl: ${ZARINPAL_CLIENT_RETURN_URL:-http://localhost:1500} + ports: + - "${API_PORT:-1505}:5005" + healthcheck: + test: ["CMD", "wget", "-q", "-O-", "http://127.0.0.1:5005/"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + + web: + build: + context: . + dockerfile: Dockerfile + args: + # Baked into the static bundle at build time. Must be the address the + # BROWSER uses to reach the API (host-mapped api port, or LAN IP). + NEXT_PUBLIC_USE_SERVER: "1" + NEXT_PUBLIC_SERVER_URL: ${NEXT_PUBLIC_SERVER_URL:-http://localhost:1505} + image: hokm-web:latest + container_name: hokm-web + restart: unless-stopped + depends_on: + server: + condition: service_healthy + ports: + - "${WEB_PORT:-1500}:80" + healthcheck: + test: ["CMD", "wget", "-q", "-O-", "http://127.0.0.1/"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 10s + +volumes: + hokm_db_data: diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..12811dc --- /dev/null +++ b/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Static Next.js export: serve the file, its .html twin, or fall back to the + # SPA shell (the app uses client-side hash routing). + location / { + try_files $uri $uri.html $uri/ /index.html; + } + + # Long-cache immutable build assets. + location /_next/static/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/package-lock.json b/package-lock.json index 9f8d8f7..b1a0513 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@capacitor/app": "^8.1.0", "@capacitor/core": "^8.4.0", + "@fontsource-variable/plus-jakarta-sans": "^5.2.8", + "@fontsource-variable/vazirmatn": "^5.2.8", "@microsoft/signalr": "^10.0.0", "clsx": "^2.1.1", "framer-motion": "^12.40.0", @@ -537,6 +539,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource-variable/plus-jakarta-sans": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/plus-jakarta-sans/-/plus-jakarta-sans-5.2.8.tgz", + "integrity": "sha512-iQecBizIdZxezODNHzOn4SvvRMrZL/S8k4MEXGDynCmUrImVW0VmX+tIAMqnADwH4haXlHSXqMgU6+kcfBQJdw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/vazirmatn": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/vazirmatn/-/vazirmatn-5.2.8.tgz", + "integrity": "sha512-2YzXfH4PNOeoZsBsgJgjbnm+IC2nonGyMLX3gXLm8FQrP+wpi1uGBIWYWlEzJ0lmntgdyAc3lRViHHfZWKvbog==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", diff --git a/package.json b/package.json index 482d960..2b9087e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "dependencies": { "@capacitor/app": "^8.1.0", "@capacitor/core": "^8.4.0", + "@fontsource-variable/plus-jakarta-sans": "^5.2.8", + "@fontsource-variable/vazirmatn": "^5.2.8", "@microsoft/signalr": "^10.0.0", "clsx": "^2.1.1", "framer-motion": "^12.40.0", diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..f8ded3f --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,7 @@ +**/bin +**/obj +*.db +*.db-shm +*.db-wal +.git +*.user diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..404e4c8 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,21 @@ +# Hokm.Server (.NET 10 ASP.NET Core + SignalR) +# Build context = ./server (so Hokm.Engine + Hokm.Server are both in scope) +FROM mirror.soroushasadi.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY nuget.docker.config /tmp/nuget.config +COPY Directory.Build.props ./ +COPY src/ ./src/ +RUN dotnet restore src/Hokm.Server/Hokm.Server.csproj --configfile /tmp/nuget.config +RUN dotnet publish src/Hokm.Server/Hokm.Server.csproj -c Release -o /out --no-restore + +FROM mirror.soroushasadi.com/dotnet/aspnet:10.0 +WORKDIR /app +# aspnet image ships no wget/curl — borrow busybox so the healthcheck has wget. +COPY --from=mirror.soroushasadi.com/busybox:1.36 /bin/busybox /usr/bin/wget +COPY --from=build /out ./ +# Bind all interfaces (appsettings binds localhost only, unreachable across the port map). +ENV ASPNETCORE_URLS=http://0.0.0.0:5005 +EXPOSE 5005 +HEALTHCHECK --interval=10s --timeout=5s --retries=12 --start-period=20s \ + CMD wget -q -O- http://127.0.0.1:5005/ || exit 1 +ENTRYPOINT ["dotnet", "Hokm.Server.dll"] diff --git a/server/nuget.docker.config b/server/nuget.docker.config new file mode 100644 index 0000000..23e5834 --- /dev/null +++ b/server/nuget.docker.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/server/src/Hokm.Server/Program.cs b/server/src/Hokm.Server/Program.cs index b8b7ba8..8d2dae6 100644 --- a/server/src/Hokm.Server/Program.cs +++ b/server/src/Hokm.Server/Program.cs @@ -81,10 +81,19 @@ builder.Services builder.Services.AddAuthorization(); // --- CORS for the Next.js client --- +// Origins are config-driven (Cors:Origins, comma/semicolon/space separated) so a +// deployed web origin can be allowed via env (Cors__Origins) without a code change. +// Falls back to the local dev origins when unset. +var corsRaw = builder.Configuration["Cors:Origins"]; +var corsOrigins = string.IsNullOrWhiteSpace(corsRaw) + ? new[] + { + "http://localhost:3000", "http://localhost:3002", "http://localhost:3020", + "http://127.0.0.1:3000", "http://127.0.0.1:3002", "http://127.0.0.1:3020", + } + : corsRaw.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); builder.Services.AddCors(o => o.AddDefaultPolicy(p => p - .WithOrigins( - "http://localhost:3000", "http://localhost:3002", "http://localhost:3020", - "http://127.0.0.1:3000", "http://127.0.0.1:3002", "http://127.0.0.1:3020") + .WithOrigins(corsOrigins) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials())); diff --git a/src/app/globals.css b/src/app/globals.css index c916049..2294349 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -39,6 +39,12 @@ --font-sans: var(--font-vazir), var(--font-jakarta), system-ui, sans-serif; } +:root { + /* Self-hosted @fontsource families (see app/layout.tsx imports). */ + --font-vazir: "Vazirmatn Variable", system-ui, sans-serif; + --font-jakarta: "Plus Jakarta Sans Variable", system-ui, sans-serif; +} + html, body { height: 100%; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e9edf9e..9b8d42d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,10 @@ import type { Metadata, Viewport } from "next"; -import { Vazirmatn, Plus_Jakarta_Sans } from "next/font/google"; +// Self-hosted fonts (no Google fetch at build time → CI builds work offline / in Iran). +import "@fontsource-variable/vazirmatn"; +import "@fontsource-variable/plus-jakarta-sans"; import "./globals.css"; import { I18nProvider } from "@/lib/i18n"; -const vazir = Vazirmatn({ - variable: "--font-vazir", - subsets: ["arabic", "latin"], - display: "swap", -}); - -const jakarta = Plus_Jakarta_Sans({ - variable: "--font-jakarta", - subsets: ["latin"], - display: "swap", -}); - export const metadata: Metadata = { title: "برگ وسط | Barg-e Vasat — بازی حکم آنلاین", description: "برگ وسط — بازی حکم آنلاین ایرانی با حریف‌های واقعی و هوشمند (Barg-e Vasat — online Persian Hokm)", @@ -34,11 +24,7 @@ export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - + {children}