From 6f02b1a0e911c1d9d4ae851d064477ec3474a89a Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 4 Jun 2026 19:21:47 +0330 Subject: [PATCH] [Local] Dockerized local test stack + always-show OTP in Development MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add docker-compose.local.yml + Dockerfile.local (public MS images + Liara NuGet) to run the whole app with a throwaway Postgres in one command for local testing, plus LOCAL.md. OtpService now never calls Kavenegar in the Development environment and always returns the code so the login page shows it on screen — guarantees local logins work with no SMS. Production behavior unchanged. Co-Authored-By: Claude Opus 4.8 --- Dockerfile.local | 17 +++++++ LOCAL.md | 58 ++++++++++++++++++++++ docker-compose.local.yml | 50 +++++++++++++++++++ src/JobsMedical.Web/Services/OtpService.cs | 13 +++-- 4 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 Dockerfile.local create mode 100644 LOCAL.md create mode 100644 docker-compose.local.yml diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..4d085fe --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,17 @@ +# Local-test image — uses public Microsoft base images + the Liara NuGet mirror. +# (The production Dockerfile pulls everything through the Nexus mirror instead.) +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY nuget.docker.config /tmp/nuget.config +COPY src/ ./src/ +RUN dotnet restore src/JobsMedical.Web/JobsMedical.Web.csproj --configfile /tmp/nuget.config -p:NuGetAudit=false +RUN dotnet publish src/JobsMedical.Web/JobsMedical.Web.csproj -c Release -o /out --no-restore \ + /p:UseAppHost=false /p:NuGetAudit=false + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /out ./ +EXPOSE 8080 +ENV ASPNETCORE_URLS=http://+:8080 \ + DOTNET_CLI_TELEMETRY_OPTOUT=1 +ENTRYPOINT ["dotnet", "JobsMedical.Web.dll"] diff --git a/LOCAL.md b/LOCAL.md new file mode 100644 index 0000000..00a6d24 --- /dev/null +++ b/LOCAL.md @@ -0,0 +1,58 @@ +# Run همکادر locally with Docker (for testing) + +A self-contained stack — app built from source + its own Postgres. It does **not** touch +production and does **not** send SMS. + +## Start + +```bash +docker compose -f docker-compose.local.yml up --build +``` + +Then open **http://localhost:8080**. + +First run takes a few minutes (pulls the .NET images, restores NuGet from the Liara mirror, +builds, then applies EF migrations + seeds demo data on startup). + +## Log in (OTP shown on screen — no SMS) + +Because it runs in the **Development** environment, the login code is printed on the page +instead of being texted: + +1. Go to **ورود / ثبت‌نام**, choose **کادر درمان**, enter **`09120000000`** (the admin phone). +2. Press **دریافت کد تأیید** → the 5-digit code appears in a green box on the page. +3. Enter it → you're in as **admin** (you'll see پنل مدیریت / تنظیمات in the nav). + +> Kavenegar is never called in Development, even if SMS is toggled on. + +## Test ingestion + +1. Go to **پنل مدیریت → تنظیمات → منابع جمع‌آوری**. +2. Enable a source and fill its config, e.g. **مدجابز (medjobs.ir)** or **تلگرام** (channel + usernames). For Telegram from inside Iran you'll need the proxy — tick **«از پروکسی استفاده شود»** + under that source and set the proxy address (see `deploy/xray/README.md`); locally you can run + your own Xray and point it at `socks5://host.docker.internal:PORT`. +3. Save, then trigger a run from **پنل مدیریت → صف آگهی‌ها** (Run-now), or set + **«اجرای خودکار»** with a short interval. +4. Watch logs: `docker compose -f docker-compose.local.yml logs -f app` + New items land in the review queue (Manual mode) or publish (Automatic mode). + +## Inspect the database + +Exposed on host port **5434** (so it won't clash with a dev DB on 5433): + +```bash +docker exec -it hamkadr_local_db psql -U hamkadr -d hamkadr +``` + +## Stop / reset + +```bash +docker compose -f docker-compose.local.yml down # stop (keeps data) +docker compose -f docker-compose.local.yml down -v # stop + wipe the DB volume +``` + +## Notes +- If `mcr.microsoft.com` isn't reachable on your machine, edit `Dockerfile.local` and swap the + two `FROM mcr.microsoft.com/dotnet/...` lines for `mirror.soroushasadi.com/dotnet/...`. +- Same for the Postgres image (`postgres:16-alpine` → `mirror.soroushasadi.com/postgres:16-alpine`). diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..52059b4 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,50 @@ +# Local test stack — build from source + a throwaway Postgres. Nothing here touches production. +# +# docker compose -f docker-compose.local.yml up --build +# → open http://localhost:8080 +# +# Runs in the Development environment, so: +# • the login OTP code is shown ON SCREEN (no SMS sent via Kavenegar) +# • sample Tehran demo data (facilities/shifts/jobs) is seeded automatically +# Admin login: phone 09120000000 → request code → the code appears on the page. +name: hamkadr-local + +services: + app: + build: + context: . + dockerfile: Dockerfile.local + container_name: hamkadr_local_app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + ports: + - "8080:8080" + environment: + ASPNETCORE_ENVIRONMENT: "Development" # ← OTP on screen, demo data, no Kavenegar + ASPNETCORE_URLS: "http://+:8080" + ConnectionStrings__Default: "Host=db;Port=5432;Database=hamkadr;Username=hamkadr;Password=hamkadr_local" + Auth__AdminPhone: "09120000000" + DOTNET_CLI_TELEMETRY_OPTOUT: "1" + + db: + image: postgres:16-alpine + container_name: hamkadr_local_db + restart: unless-stopped + environment: + POSTGRES_DB: hamkadr + POSTGRES_USER: hamkadr + POSTGRES_PASSWORD: hamkadr_local + ports: + - "5434:5432" # exposed for inspection; 5434 avoids clashing with your dev DB on 5433 + volumes: + - hamkadr_local_db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U hamkadr -d hamkadr"] + interval: 5s + timeout: 5s + retries: 20 + +volumes: + hamkadr_local_db: diff --git a/src/JobsMedical.Web/Services/OtpService.cs b/src/JobsMedical.Web/Services/OtpService.cs index f90bacb..f712551 100644 --- a/src/JobsMedical.Web/Services/OtpService.cs +++ b/src/JobsMedical.Web/Services/OtpService.cs @@ -1,5 +1,6 @@ using JobsMedical.Web.Services.Scraping; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Hosting; namespace JobsMedical.Web.Services; @@ -13,12 +14,14 @@ public class OtpService private readonly IMemoryCache _cache; private readonly ISmsSender _sms; private readonly SettingsService _settings; + private readonly IHostEnvironment _env; - public OtpService(IMemoryCache cache, ISmsSender sms, SettingsService settings) + public OtpService(IMemoryCache cache, ISmsSender sms, SettingsService settings, IHostEnvironment env) { _cache = cache; _sms = sms; _settings = settings; + _env = env; } private static string Key(string phone) => $"otp:{Normalize(phone)}"; @@ -33,12 +36,14 @@ public class OtpService _cache.Set(Key(phone), code, TimeSpan.FromMinutes(5)); var settings = await _settings.GetAsync(); - if (settings.SmsEnabled) + // In Development (local Docker / dotnet run) never call the SMS gateway — always show the + // code on the login screen. In Production, send via SMS only when it's enabled. + if (settings.SmsEnabled && !_env.IsDevelopment()) { await _sms.SendOtpAsync(phone, code, settings); - return null; // never reveal the code in production + return null; // sent via SMS — don't reveal it } - return code; // dev: surface it on screen + return code; // local/dev (or SMS off): surface it on screen } public bool Verify(string phone, string code)