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)