diff --git a/.env.example b/.env.example index d14a0f8..ebcd130 100644 --- a/.env.example +++ b/.env.example @@ -1,52 +1,85 @@ -# Copy to .env and adjust if ports conflict on your machine: -# copy .env.example .env +# ───────────────────────────────────────────────────────────────────────────── +# Meezi — environment template +# Copy to .env and fill in values. NEVER commit .env to git. +# +# For production: put the full contents in Gitea → Settings → Secrets → ENV_FILE +# ───────────────────────────────────────────────────────────────────────────── -# Host ports (what you open in the browser) -WEB_PORT=3101 # Dashboard http://localhost:3101/fa/login -WEBSITE_PORT=3010 # Website http://localhost:3010/fa -ADMIN_WEB_PORT=3102 # Admin panel http://localhost:3102/fa/admin/login -API_PORT=5080 # Main API http://localhost:5080/swagger -ADMIN_API_PORT=5081 # Admin API http://localhost:5081/swagger +# ── Environment ─────────────────────────────────────────────────────────────── +ASPNETCORE_ENVIRONMENT=Production -# Optional: expose DB/Redis on host (for local tools). Change if already in use. +# ── Database ────────────────────────────────────────────────────────────────── +DB_PASSWORD=change-me-strong-password +DB_CONNECTION_STRING=Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=change-me-strong-password + +# ── JWT ─────────────────────────────────────────────────────────────────────── +# openssl rand -hex 32 +JWT_KEY=change-me-64-char-random-string-use-openssl-rand-hex-32-output + +# ── TODAY: IP-based access (no domain yet) ─────────────────────────────────── +# Replace 171.22.25.73 with your actual server IP. +# Note: NEXT_PUBLIC_* are baked into Next.js images at build time. +# When you switch to a domain tomorrow, update these AND re-run CI (to rebuild). + +NEXT_PUBLIC_API_URL=http://171.22.25.73:5080 +NEXT_PUBLIC_ADMIN_API_URL=http://171.22.25.73:5081 +NEXT_PUBLIC_SITE_URL=http://171.22.25.73:3010 +NEXT_PUBLIC_FINDER_URL=http://171.22.25.73:3103 + +APP_QR_BASE_URL=http://171.22.25.73:3101 +BILLING_DASHBOARD_URL=http://171.22.25.73:3101 + +CORS_ORIGIN_0=http://171.22.25.73:3101 +CORS_ORIGIN_1=http://171.22.25.73:3010 +CORS_ORIGIN_2=http://171.22.25.73:3103 +CORS_ADMIN_ORIGIN_0=http://171.22.25.73:3102 + +# Host ports (what gets exposed on the server) +API_PORT=5080 +ADMIN_API_PORT=5081 +WEB_PORT=3101 +ADMIN_WEB_PORT=3102 +WEBSITE_PORT=3010 +FINDER_PORT=3103 POSTGRES_PORT=5434 REDIS_PORT=6381 -# Browser must reach the API on the host (not Docker service names) -NEXT_PUBLIC_API_URL=http://localhost:5080 -NEXT_PUBLIC_ADMIN_API_URL=http://localhost:5081 +# ── TOMORROW: domain + Caddy (comment out IP section above, use this) ───────── +# DOMAIN=meezi.ir +# ACME_EMAIL=you@example.com +# +# NEXT_PUBLIC_API_URL=https://api.meezi.ir +# NEXT_PUBLIC_ADMIN_API_URL=https://admin-api.meezi.ir +# NEXT_PUBLIC_SITE_URL=https://meezi.ir +# NEXT_PUBLIC_FINDER_URL=https://finder.meezi.ir +# +# APP_QR_BASE_URL=https://app.meezi.ir +# BILLING_DASHBOARD_URL=https://app.meezi.ir +# +# CORS_ORIGIN_0=https://app.meezi.ir +# CORS_ORIGIN_1=https://meezi.ir +# CORS_ORIGIN_2=https://finder.meezi.ir +# CORS_ADMIN_ORIGIN_0=https://admin.meezi.ir +# +# Then run CI once to rebuild images with the new URLs baked in. +# DNS required: meezi.ir, app.meezi.ir, api.meezi.ir, +# finder.meezi.ir, admin.meezi.ir, admin-api.meezi.ir → server IP -# Marketing website — public URL (used for sitemap, JSON-LD, canonical) -NEXT_PUBLIC_SITE_URL=http://localhost:3010 +# ── Migrations ──────────────────────────────────────────────────────────────── +RUN_MIGRATIONS=true -# API Docker base images (if build fails — see docs/DOCKER.md) -# DOTNET_SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:10.0 -# DOTNET_ASPNET_IMAGE=mcr.microsoft.com/dotnet/aspnet:10.0 - -# --- API (docker-compose / Arvan) --- -# ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=... -# ConnectionStrings__Redis=redis:6379 -# Jwt__Key=<32+ char secret> -# App__PublicBaseUrl=http://localhost:5080 -# App__QrPublicBaseUrl=http://localhost:3101 -# Billing__DashboardBaseUrl=http://localhost:3101 -# RUN_MIGRATIONS=true - -# ZarinPal (empty = mock payment in dev) +# ── Payment: ZarinPal ───────────────────────────────────────────────────────── # Get your merchant ID from: https://panel.zarinpal.com → API → MerchantID ZARINPAL_MERCHANT_ID= -ZARINPAL_SANDBOX=true +ZARINPAL_SANDBOX=false -# Snappfood webhook HMAC secret (dev default in appsettings) -# Snappfood__WebhookSecret=meezi-dev-snappfood-secret +# ── SMS: Kavenegar ──────────────────────────────────────────────────────────── +# Empty = OTP is logged to API console (fine for dev, not for production) +KAVENEGAR_API_KEY= -# Taraz / سامانه مودیان (optional; stub without cert) -# Taraz__Username= -# Taraz__Password= -# Taraz__CertificatePath= +# ── Snappfood webhook ───────────────────────────────────────────────────────── +SNAPPFOOD_WEBHOOK_SECRET=change-me-snappfood-secret -# Kavenegar SMS (empty = OTP logged to API console in dev) -# Kavenegar__ApiKey= - -# CORS (comma-separated origins for production) -# Cors__Origins__0=https://app.meezi.ir +# ── Docker image overrides (if direct MCR pull fails) ──────────────────────── +# DOTNET_SDK_IMAGE=171.22.25.73:5002/dotnet/sdk:10.0 +# DOTNET_ASPNET_IMAGE=171.22.25.73:5002/dotnet/aspnet:10.0 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..d1e8a6e --- /dev/null +++ b/Caddyfile @@ -0,0 +1,43 @@ +# Meezi — Caddy reverse proxy +# +# Set DOMAIN and ACME_EMAIL in your .env, then: +# docker compose -f docker-compose.yml -f docker-compose.admin.yml -f docker-compose.caddy.yml up -d +# +# Caddy auto-provisions Let's Encrypt TLS — no certbot needed. +# Domains needed in DNS (all → same server IP): +# meezi.ir, app.meezi.ir, api.meezi.ir, +# finder.meezi.ir, admin.meezi.ir, admin-api.meezi.ir + +{ + email {$ACME_EMAIL} +} + +# ── Marketing website ──────────────────────────────────────────────────────── +{$DOMAIN} { + reverse_proxy website:3000 +} + +# ── Cafe owner dashboard ───────────────────────────────────────────────────── +app.{$DOMAIN} { + reverse_proxy web:3000 +} + +# ── Main API ───────────────────────────────────────────────────────────────── +api.{$DOMAIN} { + reverse_proxy api:8080 +} + +# ── Finder (public discovery) ──────────────────────────────────────────────── +finder.{$DOMAIN} { + reverse_proxy finder:3000 +} + +# ── Super-Admin panel ──────────────────────────────────────────────────────── +admin.{$DOMAIN} { + reverse_proxy admin-web:3000 +} + +# ── Super-Admin API ────────────────────────────────────────────────────────── +admin-api.{$DOMAIN} { + reverse_proxy admin-api:8080 +} diff --git a/docker-compose.admin.yml b/docker-compose.admin.yml index 8fe5db4..d85c305 100644 --- a/docker-compose.admin.yml +++ b/docker-compose.admin.yml @@ -1,12 +1,12 @@ -# Meezi platform admin — use WITH main stack (shared Postgres + Redis) +# Meezi admin stack — overlay on top of main compose # -# docker compose up -d postgres redis +# Requires main stack (postgres + redis) to be running. +# Usage: # docker compose -f docker-compose.yml -f docker-compose.admin.yml up -d --build # # URLs: -# Admin web http://localhost:3102/fa/admin/login -# Admin API http://localhost:5081/swagger -# Health http://localhost:5081/health +# Admin panel http://SERVER:3102/fa/admin/login +# Admin API http://SERVER:5081/swagger services: admin-api: @@ -24,14 +24,15 @@ services: redis: condition: service_healthy environment: - ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Development}" ASPNETCORE_URLS: http://+:8080 RUN_MIGRATIONS: "false" - ConnectionStrings__DefaultConnection: Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass + ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}" ConnectionStrings__Redis: redis:6379 - Cors__Origins__0: http://localhost:${ADMIN_WEB_PORT:-3102} - Cors__Origins__1: http://localhost:3101 - Kavenegar__ApiKey: "" + Jwt__Key: "${JWT_KEY:-dev-jwt-key-CHANGE-THIS-IN-PRODUCTION-min32chars}" + Cors__Origins__0: "${CORS_ADMIN_ORIGIN_0:-http://localhost:3102}" + Cors__Origins__1: "${CORS_ORIGIN_0:-http://localhost:3101}" + Kavenegar__ApiKey: "${KAVENEGAR_API_KEY:-}" ports: - "${ADMIN_API_PORT:-5081}:8080" healthcheck: diff --git a/docker-compose.caddy.yml b/docker-compose.caddy.yml new file mode 100644 index 0000000..c9fae16 --- /dev/null +++ b/docker-compose.caddy.yml @@ -0,0 +1,47 @@ +# Caddy reverse proxy overlay — use when you have a domain + DNS pointing at this server. +# +# Usage: +# docker compose \ +# -f docker-compose.yml \ +# -f docker-compose.admin.yml \ +# -f docker-compose.caddy.yml \ +# up -d +# +# Required in .env: +# DOMAIN=meezi.ir +# ACME_EMAIL=you@example.com +# +# After adding this, update .env URLs from http://IP:PORT to https://subdomain.DOMAIN +# and re-run CI (Next.js bakes NEXT_PUBLIC_* at build time → rebuild required). +# +# Firewall: open 80 + 443, keep 3101/3102/3103/5080/5081 blocked from internet. + +services: + caddy: + image: caddy:2-alpine + container_name: meezi-caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" # HTTP/3 + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + environment: + DOMAIN: "${DOMAIN}" + ACME_EMAIL: "${ACME_EMAIL}" + depends_on: + - api + - web + - website + - finder + - admin-api + - admin-web + +volumes: + caddy_data: + name: meezi-caddy-data + caddy_config: + name: meezi-caddy-config diff --git a/docker-compose.full.yml b/docker-compose.full.yml index d03799f..8a18528 100644 --- a/docker-compose.full.yml +++ b/docker-compose.full.yml @@ -81,8 +81,8 @@ services: Kavenegar__ApiKey: "" Billing__DashboardBaseUrl: http://localhost:${WEB_PORT:-3101} Snappfood__WebhookSecret: meezi-dev-snappfood-secret - ZarinPal__MerchantId: "104c093d-2f5b-470d-978b-e4edefbf6cc8" - ZarinPal__Sandbox: "true" + ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}" + ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}" ports: - "${API_PORT:-5080}:8080" volumes: diff --git a/docker-compose.yml b/docker-compose.yml index b5c742a..c5bd71a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,20 @@ -# Meezi — full stack (Postgres, Redis, API, Dashboard, Marketing Website) +# Meezi — main stack (Postgres, Redis, API, Dashboard, Website, Finder) # -# Setup: -# copy .env.example .env -# powershell -File scripts/docker-up-full.ps1 -# — or — docker compose up -d --build +# Local dev: +# cp .env.example .env +# docker compose up -d --build # -# If image pulls fail (Iran / MCR timeout): VPN on, or see docs/DOCKER.md +# Production (IP-based, no domain yet): +# Set ENV_FILE secret in Gitea — CI writes .env and runs docker compose up -d # -# URLs (defaults): -# Dashboard http://localhost:3101/fa/login -# Website http://localhost:3010/fa -# Finder http://localhost:3103/fa -# API http://localhost:5080/swagger -# Health http://localhost:5080/health +# Production (with domain, add Caddy): +# docker compose -f docker-compose.yml -f docker-compose.admin.yml -f docker-compose.caddy.yml up -d +# +# URLs (port-based defaults): +# Dashboard http://SERVER:3101/fa/login +# Website http://SERVER:3010/fa +# Finder http://SERVER:3103/fa +# API http://SERVER:5080/swagger services: postgres: @@ -22,7 +24,7 @@ services: environment: POSTGRES_DB: meezi POSTGRES_USER: meezi - POSTGRES_PASSWORD: meezi_local_pass + POSTGRES_PASSWORD: "${DB_PASSWORD:-meezi_local_pass}" ports: - "${POSTGRES_PORT:-5434}:5432" volumes: @@ -63,20 +65,21 @@ services: redis: condition: service_healthy environment: - ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Development}" ASPNETCORE_URLS: http://+:8080 - RUN_MIGRATIONS: "true" - ConnectionStrings__DefaultConnection: Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass + RUN_MIGRATIONS: "${RUN_MIGRATIONS:-true}" + ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}" ConnectionStrings__Redis: redis:6379 - App__PublicBaseUrl: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} - App__QrPublicBaseUrl: http://localhost:${WEB_PORT:-3101} - Cors__Origins__0: http://localhost:${WEB_PORT:-3101} - Cors__Origins__1: http://localhost:${WEBSITE_PORT:-3010} - Cors__Origins__2: http://localhost:${FINDER_PORT:-3103} - Auth__MaxOtpAttemptsPerHour: "100" - Kavenegar__ApiKey: "" - Billing__DashboardBaseUrl: http://localhost:${WEB_PORT:-3101} - Snappfood__WebhookSecret: meezi-dev-snappfood-secret + Jwt__Key: "${JWT_KEY:-dev-jwt-key-CHANGE-THIS-IN-PRODUCTION-min32chars}" + App__PublicBaseUrl: "${NEXT_PUBLIC_API_URL:-http://localhost:5080}" + App__QrPublicBaseUrl: "${APP_QR_BASE_URL:-http://localhost:3101}" + Billing__DashboardBaseUrl: "${BILLING_DASHBOARD_URL:-http://localhost:3101}" + Cors__Origins__0: "${CORS_ORIGIN_0:-http://localhost:3101}" + Cors__Origins__1: "${CORS_ORIGIN_1:-http://localhost:3010}" + Cors__Origins__2: "${CORS_ORIGIN_2:-http://localhost:3103}" + Auth__MaxOtpAttemptsPerHour: "${OTP_RATE_LIMIT:-100}" + Kavenegar__ApiKey: "${KAVENEGAR_API_KEY:-}" + Snappfood__WebhookSecret: "${SNAPPFOOD_WEBHOOK_SECRET:-meezi-dev-snappfood-secret}" ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}" ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}" ports: @@ -84,7 +87,6 @@ services: volumes: - api_uploads:/app/uploads healthcheck: - # TCP probe only — no apt-get/curl in image (build works offline / without Ubuntu mirrors) test: ["CMD-SHELL", "bash -c 'cat /dev/tcp/127.0.0.1/8080' || exit 1"] interval: 10s timeout: 5s @@ -124,7 +126,7 @@ services: PORT: "3000" HOSTNAME: 0.0.0.0 MEEZI_API_URL: http://api:8080 - NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3010} + NEXT_PUBLIC_SITE_URL: "${NEXT_PUBLIC_SITE_URL:-http://localhost:3010}" ports: - "${WEBSITE_PORT:-3010}:3000" @@ -143,8 +145,8 @@ services: environment: PORT: "3000" HOSTNAME: 0.0.0.0 - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:5080} - NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_FINDER_URL:-http://localhost:3103} + NEXT_PUBLIC_API_URL: "${NEXT_PUBLIC_API_URL:-http://localhost:5080}" + NEXT_PUBLIC_SITE_URL: "${NEXT_PUBLIC_FINDER_URL:-http://localhost:3103}" ports: - "${FINDER_PORT:-3103}:3000"