diff --git a/.dockerignore b/.dockerignore index 2ebfc6f..d9af492 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,14 +11,8 @@ **/.cache **/*.tsbuildinfo -**/.env -**/.env.local -**/.env.docker -**/.env.development.local -**/.env.test.local -**/.env.production.local -!**/.env.example -!**/.env.docker.example +**/.env* +!**/.env*.example *.log npm-debug.log* diff --git a/.env.example b/.env.example index b8374db..7711132 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,11 @@ -# Docker Compose build-time variable substitution +# FinTrack root — Docker Compose build-arg template. # Copy this file to .env and fill in your values. -# These variables are used by docker compose for build args (not runtime). +# Used by docker compose for build args at image build time (not runtime). -# Telegram Login Widget — baked into the Next.js web build -# Numeric bot id — digits before the ":" in the bot token (e.g. 123456789:ABC -> 123456789) +# ═════════════════════════════════════════════════════════════ +# REQUIRED — web image won't build correctly without this +# ═════════════════════════════════════════════════════════════ + +# Telegram Login Widget — baked into the Next.js web build. +# Numeric bot id — digits before the ":" in the bot token (e.g. 123456789:ABC -> 123456789). NEXT_PUBLIC_TELEGRAM_BOT_ID="your_numeric_bot_id" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a3fe08..5b4fa00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,16 +11,6 @@ concurrency: cancel-in-progress: true jobs: - validate-env: - name: Validate ENV Docs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Check if .env.example matches .env.docker.example - run: | - diff <(grep -v '^#' apps/api/.env.example | cut -d= -f1 | sort) <(grep -v '^#' apps/api/.env.docker.example | cut -d= -f1 | sort) || { echo "::error::API env examples are out of sync!"; exit 1; } - shell: bash - migration-check: name: Migration Drift Check runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f213db2..ccb9ef4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ cd FinTrack # Use the dx CLI to create all necessary .env files from examples bash dx setup -# → Edit .env for local run or .env.docker for Docker setup (DATABASE_URL host difference). +# → Edit each apps/*/.env. In Docker, compose overrides the PostgreSQL/Redis/Mongo hosts. ``` ### 2. Docker (Recommended) @@ -166,18 +166,16 @@ pnpm run dev `bash dx setup` copies all example files automatically. For manual setup: -| File | Example | Notes | -| --------------------------- | ----------------------------------- | ------------------------------------------------------------ | -| `.env` (repo root) | `.env.example` | Docker Compose build args — `NEXT_PUBLIC_TELEGRAM_BOT_ID` | -| `apps/api/.env` | `apps/api/.env.example` | Local dev — fill in secrets | -| `apps/api/.env.docker` | `apps/api/.env.docker.example` | Docker dev — DB host is `postgres` | -| `apps/api/.env.test` | `apps/api/.env.test.example` | Local tests — points to `fintrack_test` | -| `apps/api/.env.test.docker` | `apps/api/.env.test.docker.example` | Docker tests — DB host is `postgres` | -| `apps/web/.env` | `apps/web/.env.example` | Set `NEXT_PUBLIC_API_URL`, `NEXTAUTH_SECRET`, Google OAuth | -| `apps/bot/.env` | `apps/bot/.env.example` | Local dev — set `TELEGRAM_BOT_TOKEN`, `API_URL`, `REDIS_URL` | -| `apps/bot/.env.docker` | `apps/bot/.env.docker.example` | Docker dev — same vars, host points to Docker service | +| File | Example | Notes | +| -------------------- | ---------------------------- | ------------------------------------------------------------------------- | +| `.env` (repo root) | `.env.example` | Docker Compose build args — `NEXT_PUBLIC_TELEGRAM_BOT_ID` | +| `apps/api/.env` | `apps/api/.env.example` | Dev + Docker — secrets; Docker hosts injected by compose | +| `apps/api/.env.test` | `apps/api/.env.test.example` | Tests — `fintrack_test`; Docker hosts rewritten by `scripts/test-env.cjs` | +| `apps/web/.env` | `apps/web/.env.example` | Set `NEXT_PUBLIC_API_URL`, `NEXTAUTH_SECRET`, Google OAuth | +| `apps/bot/.env` | `apps/bot/.env.example` | Dev + Docker — `TELEGRAM_BOT_TOKEN`; Docker hosts injected by compose | -Each example file is annotated — read it for variable descriptions and required values. +Each example file is split into REQUIRED (app won't boot without these) and +OPTIONAL (safe defaults + feature toggles) blocks — read it for per-variable details. --- @@ -257,7 +255,8 @@ The test database (`fintrack_test`) is separate from the dev DB and is used excl cd apps/api cp .env.example .env -# fill in DATABASE_URL, ACCESS_TOKEN_SECRET, GROQ_API_KEY_1, STRIPE_*, GOOGLE_CLIENT_ID, TELEGRAM_BOT_TOKEN ... +# fill in the REQUIRED block (DATABASE_URL, ACCESS_TOKEN_SECRET, GOOGLE_CLIENT_ID, TELEGRAM_BOT_TOKEN ...); +# OPTIONAL vars (GROQ_API_KEY_1, STRIPE_*, ...) gate features and can stay empty pnpm run prisma:migrate:dev # apply migrations pnpm run prisma:seed # optional seed data @@ -343,10 +342,9 @@ Every tier has a `:dx` variant that targets the Docker environment (e.g. `test:l ### Environment Files -| File | Used by | DB host | -| --------------------------- | ---------------------- | --------------------------- | -| `apps/api/.env.test` | local `test:*` scripts | `localhost` | -| `apps/api/.env.test.docker` | `test:*:dx` scripts | `postgres` (Docker service) | +| File | Used by | DB host | +| -------------------- | ------------------------------- | ------------------------------------------------------------------------------ | +| `apps/api/.env.test` | local + Docker `test:*` scripts | `localhost`; `:dx` scripts rewrite it to `postgres` via `scripts/test-env.cjs` | ### Running Tests diff --git a/README.md b/README.md index 5f2a61d..771f8aa 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ The database uses **PostgreSQL 15** managed via Prisma migrations. Key models: | `donation` | `/donations` | Create Stripe Checkout session, webhook handler, leaderboard | | `admin` | `/admin` | User list, role update, session revocation, error log management, stats | -Interactive Swagger docs are available at `/api-docs` (`ENABLE_SWAGGER_IN_PROD=true` or in dev mode). +Interactive Swagger docs are served at `/api-docs` automatically in dev and test; in production they are exposed only when `ENABLE_SWAGGER_IN_PROD=true`. ### Security @@ -278,17 +278,16 @@ Database migrations are applied automatically from GitHub Actions on pushes to ` GitHub Actions runs the following checks on every pull request and push to `master`/`main`: -1. **ENV Docs Validation** — verifies `apps/api/.env.example` and `apps/api/.env.docker.example` stay in sync. -2. **Migration Drift** — non-blocking check that `schema.prisma` matches migrations (advisory). -3. **Format & Lint** — `prettier`, `eslint`, and ToC freshness check. -4. **Type check** — `tsc --noEmit` across the monorepo. -5. **Security Audit** — `pnpm audit` and dependency review. -6. **Tests** — Jest + Supertest API tests and Vitest web tests against a real PostgreSQL container. -7. **Release Gate** — on push to `master`/`main`, `gate.yml` listens for CI completion and dispatches `release.yml` only when CI succeeds; if CI fails, dispatch is skipped and a failure is logged. -8. **Release Workflow (`release.yml`)** — also triggers directly on push/PR to `master`/`main`; builds and (on push) publishes images to **GHCR**. -9. **Security Scanning** — **Trivy** scans every Docker image for vulnerabilities (CRITICAL, HIGH). -10. **CodeQL Analysis** — static analysis of JavaScript/TypeScript on every PR and push, plus a weekly scheduled scan. -11. **Prisma Auto-Migrate (`master`)** — applies `apps/api` migrations to Supabase after schema/migration changes. +1. **Migration Drift** — non-blocking check that `schema.prisma` matches migrations (advisory). +2. **Format & Lint** — `prettier`, `eslint`, and ToC freshness check. +3. **Type check** — `tsc --noEmit` across the monorepo. +4. **Security Audit** — `pnpm audit` and dependency review. +5. **Tests** — Jest + Supertest API tests and Vitest web tests against a real PostgreSQL container. +6. **Release Gate** — on push to `master`/`main`, `gate.yml` listens for CI completion and dispatches `release.yml` only when CI succeeds; if CI fails, dispatch is skipped and a failure is logged. +7. **Release Workflow (`release.yml`)** — also triggers directly on push/PR to `master`/`main`; builds and (on push) publishes images to **GHCR**. +8. **Security Scanning** — **Trivy** scans every Docker image for vulnerabilities (CRITICAL, HIGH). +9. **CodeQL Analysis** — static analysis of JavaScript/TypeScript on every PR and push, plus a weekly scheduled scan. +10. **Prisma Auto-Migrate (`master`)** — applies `apps/api` migrations to Supabase after schema/migration changes. ### Auto Migration Secrets (GitHub) diff --git a/apps/api/.env.docker.example b/apps/api/.env.docker.example deleted file mode 100644 index 5cc6519..0000000 --- a/apps/api/.env.docker.example +++ /dev/null @@ -1,56 +0,0 @@ -# Environment -NODE_ENV="production" -ENABLE_SWAGGER_IN_PROD="false" - -# App setup (inside docker network) -HOST="0.0.0.0" -PORT="8000" -SWAGGER_SERVER_URL="http://localhost:8080/api" -CORS_ORIGINS="http://localhost,http://localhost:5173,http://localhost:8080,http://localhost:8000,http://127.0.0.1:5173" -FRONTEND_URL="http://localhost:5173" - -# Docker PostgreSQL / Prisma (development DB only) -DATABASE_URL="postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public" -DIRECT_URL="postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public" - -# Redis (Docker service name) -REDIS_URL="redis://redis:6379" - -# JWT/access token -ACCESS_TOKEN_SECRET="your_jwt_access_token_secret_here" -CSRF_SECRET="your_csrf_secret_here" - -# Google OAuth verification (must match Google Cloud OAuth client) -GOOGLE_CLIENT_ID="your_google_client_id.apps.googleusercontent.com" -GOOGLE_OAUTH_VERIFY_MODE="verifyIdToken" # verifyIdToken | tokeninfo - -# Telegram Login Widget verification -TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" - -# Optional: link the seeded admin user to your real Telegram ID so the bot -# authenticates as admin and shows the rich seeded transactions. -# SEED_TELEGRAM_LINK_ID="123456789" - -# AI apis -GROQ_API_KEY_1=your_groq_api_key -API_KEY_ENCRYPTION_SECRET=your-32-char-secret-here # 32+ symb - -# Stripe donation -STRIPE_SECRET_KEY=sk_test_xxx -STRIPE_WEBHOOK_SECRET=whsec_xxx -STRIPE_DONATION_PRICE_ID=price_xxx # optional if amount/currency is used -STRIPE_DONATION_AMOUNT=300 # in minor units, e.g. 300 = $3.00 -STRIPE_DONATION_CURRENCY=usd -STRIPE_DONATION_SUCCESS_URL=http://localhost:5173/FinTrack/donation?status=success -STRIPE_DONATION_CANCEL_URL=http://localhost:5173/FinTrack/donation?status=cancel -STRIPE_DONATION_DURATION_DAYS=0 # 0 or empty = permanent donor - -# SMTP — email verification (Gmail example) -# For Gmail: enable 2FA, create App Password at myaccount.google.com/apppasswords -SMTP_HOST="smtp.gmail.com" -SMTP_PORT="587" -SMTP_SECURE="false" # true only for port 465 -SMTP_USER="your@gmail.com" -SMTP_PASS="your_app_password" #https://myaccount.google.com/apppasswords -SMTP_FROM="FinTrack " -EMAIL_VERIFICATION_BASE_URL="http://localhost:8000/api" diff --git a/apps/api/.env.example b/apps/api/.env.example index 9c4eb61..22d668f 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,56 +1,71 @@ -# Environment -NODE_ENV="development" -ENABLE_SWAGGER_IN_PROD="false" +# FinTrack API — environment template. +# Set everything under REQUIRED; OPTIONAL has safe defaults and feature toggles. -# App setup -HOST="localhost" -PORT="8000" -SWAGGER_SERVER_URL="http://localhost:8000/api" -CORS_ORIGINS="http://localhost,http://localhost:5173,http://localhost:8080,http://localhost:8000,http://127.0.0.1:5173" -FRONTEND_URL="http://localhost:5173" +# ═════════════════════════════════════════════════════════════ +# REQUIRED — app refuses to boot without these +# ═════════════════════════════════════════════════════════════ -# Local PostgreSQL / Prisma (development DB only) +# Databases — hosts target local (non-Docker) runs. In Docker they are +# rewritten to the service names (postgres/redis/mongo) by compose for the +# running app, and by scripts/test-env.cjs for tests. DATABASE_URL="postgresql://fintrack:fintrack@localhost:5432/fintrack?schema=public" DIRECT_URL="postgresql://fintrack:fintrack@localhost:5432/fintrack?schema=public" - -# Redis REDIS_URL="redis://localhost:6379" -# JWT/access token -ACCESS_TOKEN_SECRET="your_jwt_access_token_secret_here" +# Secrets & tokens +ACCESS_TOKEN_SECRET="your_access_token_secret_here" CSRF_SECRET="your_csrf_secret_here" +API_KEY_ENCRYPTION_SECRET="your_api_key_encryption_secret_here" # any non-empty value (hashed to 32 bytes) -# Google OAuth verification (must match Google Cloud OAuth client) -GOOGLE_CLIENT_ID="your_google_client_id.apps.googleusercontent.com" -GOOGLE_OAUTH_VERIFY_MODE="verifyIdToken" # verifyIdToken | tokeninfo +# Auth providers +GOOGLE_CLIENT_ID="your_google_client_id.apps.googleusercontent.com" # must match Google Cloud OAuth client +TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" # Telegram Login Widget verification + +# ═════════════════════════════════════════════════════════════ +# OPTIONAL — safe defaults; features stay off until configured +# ═════════════════════════════════════════════════════════════ + +# Runtime +NODE_ENV="development" # default: development +ENABLE_SWAGGER_IN_PROD="false" # default: false -# Telegram Login Widget verification -TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" +# Server (localhost defaults applied when unset) +HOST="localhost" +PORT="8000" +SWAGGER_SERVER_URL="http://localhost:8000/api" +CORS_ORIGINS="http://localhost,http://localhost:5173,http://localhost:8080,http://localhost:8000,http://127.0.0.1:5173" +FRONTEND_URL="http://localhost:5173" -# Optional: link the seeded admin user to your real Telegram ID so the bot +# Auth tuning +GOOGLE_OAUTH_VERIFY_MODE="verifyIdToken" # verifyIdToken | tokeninfo +# Link the seeded admin user to your real Telegram ID so the bot # authenticates as admin and shows the rich seeded transactions. # SEED_TELEGRAM_LINK_ID="123456789" -# AI apis -GROQ_API_KEY_1=your_groq_api_key -API_KEY_ENCRYPTION_SECRET=your-32-char-secret-here # 32+ symb - -# Stripe donation -STRIPE_SECRET_KEY=sk_test_xxx -STRIPE_WEBHOOK_SECRET=whsec_xxx -STRIPE_DONATION_PRICE_ID=price_xxx # optional if amount/currency is used -STRIPE_DONATION_AMOUNT=300 # in minor units, e.g. 300 = $3.00 -STRIPE_DONATION_CURRENCY=usd -STRIPE_DONATION_SUCCESS_URL=http://localhost:5173/FinTrack/donation?status=success -STRIPE_DONATION_CANCEL_URL=http://localhost:5173/FinTrack/donation?status=cancel -STRIPE_DONATION_DURATION_DAYS=0 # 0 or empty = permanent donor - -# SMTP — email verification (Gmail example) +# Feature · Audit log (MongoDB) — leave unset to disable. +# MONGO_URL="mongodb://localhost:27017/fintrack" + +# Feature · AI categorization (Groq) — without a key it is skipped (warns). +# Add more keys as GROQ_API_KEY_2, _3, … +GROQ_API_KEY_1="your_groq_api_key" + +# Feature · Donations (Stripe) — leave empty to disable the donation flow. +STRIPE_SECRET_KEY="sk_test_xxx" +STRIPE_WEBHOOK_SECRET="whsec_xxx" +STRIPE_DONATION_PRICE_ID="price_xxx" # if unset, the amount/currency path is used +STRIPE_DONATION_AMOUNT="300" # minor units, e.g. 300 = $3.00 +STRIPE_DONATION_CURRENCY="usd" +STRIPE_DONATION_SUCCESS_URL="http://localhost:5173/donation?status=success" +STRIPE_DONATION_CANCEL_URL="http://localhost:5173/donation?status=cancel" +STRIPE_DONATION_DURATION_DAYS="0" # 0 or empty = permanent donor + +# Feature · Email verification (SMTP) — empty host/user/pass makes the mailer +# skip real sending (see src/utils/mailer.ts). # For Gmail: enable 2FA, create App Password at myaccount.google.com/apppasswords SMTP_HOST="smtp.gmail.com" SMTP_PORT="587" SMTP_SECURE="false" # true only for port 465 SMTP_USER="your@gmail.com" -SMTP_PASS="your_app_password" #https://myaccount.google.com/apppasswords +SMTP_PASS="your_app_password" # https://myaccount.google.com/apppasswords SMTP_FROM="FinTrack " EMAIL_VERIFICATION_BASE_URL="http://localhost:8000/api" diff --git a/apps/api/.env.test.docker.example b/apps/api/.env.test.docker.example deleted file mode 100644 index 9ac42d8..0000000 --- a/apps/api/.env.test.docker.example +++ /dev/null @@ -1,51 +0,0 @@ -# Environment -NODE_ENV="test" -ENABLE_SWAGGER_IN_PROD="false" - -# App setup (inside docker network) -HOST="0.0.0.0" -PORT="8000" -SWAGGER_SERVER_URL="http://api:8000/api" -CORS_ORIGINS="http://localhost:5173,http://127.0.0.1:5173" -FRONTEND_URL="http://localhost:5173" - -# Docker PostgreSQL / Prisma (test DB only) -DATABASE_URL="postgresql://fintrack:fintrack@postgres:5432/fintrack_test?schema=public" -DIRECT_URL="postgresql://fintrack:fintrack@postgres:5432/fintrack_test?schema=public" - -# Redis (Docker service name) -REDIS_URL="redis://redis:6379" - -# JWT/access token -ACCESS_TOKEN_SECRET="test_access_secret" -CSRF_SECRET="test_csrf_secret" - -# Google OAuth verification -GOOGLE_CLIENT_ID="test-google-client-id.apps.googleusercontent.com" -GOOGLE_OAUTH_VERIFY_MODE="verifyIdToken" # verifyIdToken | tokeninfo - -# Telegram Login Widget verification -TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" - -# AI apis -GROQ_API_KEY_1="test_groq_key_optional" -API_KEY_ENCRYPTION_SECRET="test_api_key_encryption_secret_123456" - -# Stripe donation (test defaults) -STRIPE_SECRET_KEY="sk_test_xxx" -STRIPE_WEBHOOK_SECRET="whsec_xxx" -STRIPE_DONATION_PRICE_ID="" -STRIPE_DONATION_AMOUNT="300" -STRIPE_DONATION_CURRENCY="usd" -STRIPE_DONATION_SUCCESS_URL="http://localhost:5173/donation?status=success" -STRIPE_DONATION_CANCEL_URL="http://localhost:5173/donation?status=cancel" -STRIPE_DONATION_DURATION_DAYS="0" - -# SMTP (optional for tests) -SMTP_HOST="" -SMTP_PORT="587" -SMTP_SECURE="false" -SMTP_USER="" -SMTP_PASS="" -SMTP_FROM="" -EMAIL_VERIFICATION_BASE_URL="http://api:8000/api" diff --git a/apps/api/.env.test.example b/apps/api/.env.test.example index 07268f0..83c578e 100644 --- a/apps/api/.env.test.example +++ b/apps/api/.env.test.example @@ -1,50 +1,69 @@ -# Environment -NODE_ENV="test" -ENABLE_SWAGGER_IN_PROD="false" +# FinTrack API — test environment template. +# Set everything under REQUIRED; OPTIONAL has safe defaults and feature toggles. -# App setup -HOST="localhost" -PORT="8000" -SWAGGER_SERVER_URL="http://localhost:8000/api" -CORS_ORIGINS="http://localhost:5173,http://127.0.0.1:5173" -FRONTEND_URL="http://localhost:5173" +# ═════════════════════════════════════════════════════════════ +# REQUIRED — app refuses to boot without these +# ═════════════════════════════════════════════════════════════ -# Local PostgreSQL / Prisma (test DB only) +# Databases — hosts target local (non-Docker) runs. In Docker they are +# rewritten to the service names (postgres/redis/mongo) by compose for the +# running app, and by scripts/test-env.cjs for tests. DATABASE_URL="postgresql://fintrack:fintrack@localhost:5432/fintrack_test?schema=public" DIRECT_URL="postgresql://fintrack:fintrack@localhost:5432/fintrack_test?schema=public" - -# Redis REDIS_URL="redis://localhost:6379" -# JWT/access token -ACCESS_TOKEN_SECRET="test_access_secret" -CSRF_SECRET="test_csrf_secret" +# Secrets & tokens +ACCESS_TOKEN_SECRET="your_access_token_secret_here" +CSRF_SECRET="your_csrf_secret_here" +API_KEY_ENCRYPTION_SECRET="your_api_key_encryption_secret_here" # any non-empty value (hashed to 32 bytes) + +# Auth providers +GOOGLE_CLIENT_ID="your_google_client_id.apps.googleusercontent.com" # must match Google Cloud OAuth client +TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" # Telegram Login Widget verification + +# ═════════════════════════════════════════════════════════════ +# OPTIONAL — safe defaults; features stay off until configured +# ═════════════════════════════════════════════════════════════ + +# Runtime +NODE_ENV="test" # default: development +ENABLE_SWAGGER_IN_PROD="false" # default: false + +# Server (localhost defaults applied when unset) +HOST="localhost" +PORT="8000" +SWAGGER_SERVER_URL="http://localhost:8000/api" +CORS_ORIGINS="http://localhost,http://localhost:5173,http://localhost:8080,http://localhost:8000,http://127.0.0.1:5173" +FRONTEND_URL="http://localhost:5173" -# Google OAuth verification -GOOGLE_CLIENT_ID="test-google-client-id.apps.googleusercontent.com" +# Auth tuning GOOGLE_OAUTH_VERIFY_MODE="verifyIdToken" # verifyIdToken | tokeninfo +# Link the seeded admin user to your real Telegram ID so the bot +# authenticates as admin and shows the rich seeded transactions. +# SEED_TELEGRAM_LINK_ID="123456789" -# Telegram Login Widget verification -TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" +# Feature · Audit log (MongoDB) — leave unset to disable. +# MONGO_URL="mongodb://localhost:27017/fintrack_test" -# AI apis -GROQ_API_KEY_1="test_groq_key_optional" -API_KEY_ENCRYPTION_SECRET="test_api_key_encryption_secret_123456" +# Feature · AI categorization (Groq) — without a key it is skipped (warns). +# Add more keys as GROQ_API_KEY_2, _3, … +GROQ_API_KEY_1="your_groq_api_key" -# Stripe donation (test defaults) +# Feature · Donations (Stripe) — leave empty to disable the donation flow. STRIPE_SECRET_KEY="sk_test_xxx" STRIPE_WEBHOOK_SECRET="whsec_xxx" -STRIPE_DONATION_PRICE_ID="" -STRIPE_DONATION_AMOUNT="300" +STRIPE_DONATION_PRICE_ID="" # empty in tests → exercises the amount/currency path (see donation/service.ts) +STRIPE_DONATION_AMOUNT="300" # minor units, e.g. 300 = $3.00 STRIPE_DONATION_CURRENCY="usd" STRIPE_DONATION_SUCCESS_URL="http://localhost:5173/donation?status=success" STRIPE_DONATION_CANCEL_URL="http://localhost:5173/donation?status=cancel" -STRIPE_DONATION_DURATION_DAYS="0" +STRIPE_DONATION_DURATION_DAYS="0" # 0 or empty = permanent donor -# SMTP (optional for tests) +# Feature · Email verification (SMTP) — left empty in tests on purpose: empty +# host/user/pass makes the mailer skip real sending (see src/utils/mailer.ts). SMTP_HOST="" SMTP_PORT="587" -SMTP_SECURE="false" +SMTP_SECURE="false" # true only for port 465 SMTP_USER="" SMTP_PASS="" SMTP_FROM="" diff --git a/apps/api/package.json b/apps/api/package.json index 59540ab..47ad330 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,19 +11,19 @@ "start:prod": "node_modules/.bin/prisma migrate deploy --schema prisma/schema.prisma && node dist/server.js", "prod": "dotenv -e .env.production.local -- concurrently \"pnpm exec tsc -w\" \"nodemon dist/server.js\"", "test:unit": "dotenv -e .env.test -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/unit", - "test:unit:dx": "dotenv -o -e .env.test.docker -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/unit", + "test:unit:dx": "node ../../scripts/test-env.cjs node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/unit", "test:integration": "dotenv -e .env.test -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/integration", - "test:integration:dx": "dotenv -o -e .env.test.docker -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/integration", + "test:integration:dx": "node ../../scripts/test-env.cjs node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/integration", "test:light": "pnpm run test:unit && pnpm run test:integration", "test:light:dx": "pnpm run test:unit:dx && pnpm run test:integration:dx", "test:stress": "dotenv -e .env.test -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/stress", - "test:stress:dx": "dotenv -o -e .env.test.docker -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/stress", + "test:stress:dx": "node ../../scripts/test-env.cjs node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/stress", "test:e2e": "dotenv -e .env.test -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/e2e", - "test:e2e:dx": "dotenv -o -e .env.test.docker -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/e2e", + "test:e2e:dx": "node ../../scripts/test-env.cjs node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathPattern=test/e2e", "test": "dotenv -e .env.test -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", - "test:dx": "dotenv -o -e .env.test.docker -- node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", + "test:dx": "node ../../scripts/test-env.cjs node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", "test:watch": "dotenv -e .env.test -- jest --watch", - "test:watch:dx": "dotenv -o -e .env.test.docker -- jest --watch", + "test:watch:dx": "node ../../scripts/test-env.cjs jest --watch", "check-types": "tsc --noEmit", "predev": "pnpm run build", "prisma": "prisma", @@ -31,7 +31,7 @@ "prisma:migrate:dev": "pnpm run prisma -- migrate dev", "prisma:migrate:deploy": "pnpm run prisma -- migrate deploy", "prisma:migrate:deploy:test:local": "dotenv -e .env.test -- pnpm run prisma:migrate:deploy", - "prisma:migrate:deploy:test:dx": "dotenv -o -e .env.test.docker -- pnpm run prisma:migrate:deploy", + "prisma:migrate:deploy:test:dx": "node ../../scripts/test-env.cjs pnpm run prisma:migrate:deploy", "prisma:studio": "pnpm run prisma -- studio --hostname 0.0.0.0", "prisma:seed": "tsx src/prisma/seed.ts" }, @@ -72,7 +72,7 @@ "ioredis": "5.10.1", "jsonwebtoken": "^9.0.2", "mongoose": "9.6.3", - "nodemailer": "^8.0.11", + "nodemailer": "^9.0.1", "openai": "^6.42.0", "pino": "10.3.1", "prisma": "^6.19.3", @@ -87,7 +87,7 @@ "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/express-serve-static-core": "4.19.0", + "@types/express-serve-static-core": "5.1.1", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.3.0", diff --git a/apps/bot/.env.docker.example b/apps/bot/.env.docker.example deleted file mode 100644 index 5033385..0000000 --- a/apps/bot/.env.docker.example +++ /dev/null @@ -1,11 +0,0 @@ -# Environment -NODE_ENV="production" - -# Telegram bot -TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" - -# API -API_URL="http://api:8000/api" - -# Redis (Docker service name) -REDIS_URL="redis://redis:6379" \ No newline at end of file diff --git a/apps/bot/.env.example b/apps/bot/.env.example index f2a456c..1f9866e 100644 --- a/apps/bot/.env.example +++ b/apps/bot/.env.example @@ -1,11 +1,19 @@ -# Environment -NODE_ENV="development" +# FinTrack Bot — environment template. +# Set everything under REQUIRED; OPTIONAL has safe defaults. + +# ═════════════════════════════════════════════════════════════ +# REQUIRED — bot refuses to boot without these +# ═════════════════════════════════════════════════════════════ -# Telegram bot TELEGRAM_BOT_TOKEN="123456789:your_telegram_bot_token" -# API -API_URL="http://api:8000/api" +# Hosts target local (non-Docker) runs. In Docker they are injected by compose +# (API_URL → api, REDIS_URL → redis — see compose.dev.yaml / compose.yaml). +API_URL="http://localhost:8000/api" +REDIS_URL="redis://localhost:6379" + +# ═════════════════════════════════════════════════════════════ +# OPTIONAL — safe defaults +# ═════════════════════════════════════════════════════════════ -# Redis -REDIS_URL="redis://localhost:6379" \ No newline at end of file +NODE_ENV="development" # default: development diff --git a/apps/web/.env.docker.example b/apps/web/.env.docker.example deleted file mode 100644 index a9ef8ed..0000000 --- a/apps/web/.env.docker.example +++ /dev/null @@ -1,27 +0,0 @@ -# Environment -NODE_ENV=development - -# Main API URL -NEXT_PUBLIC_API_URL=http://localhost:8000/api -NEXT_PUBLIC_SITE_ORIGIN=http://localhost:5173 - -# NextAuth -NEXTAUTH_URL=http://localhost:5173/FinTrack/api/auth -NEXTAUTH_SECRET=generate_random_32_chars_string_here - -# Google OAuth -GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=your_google_client_secret - -# Telegram Login Widget -# Numeric bot id — digits before the ":" in the bot token (e.g. 123456789:ABC -> 123456789) -NEXT_PUBLIC_TELEGRAM_BOT_ID=your_numeric_bot_id - -# ============================================ -# -# docker run -d --name fintrack-web -p 5173:5173 --add-host=host.docker.internal:host-gateway --env-file apps/web/.env.docker fintrack-web -# -# docker build --build-arg NEXT_PUBLIC_API_URL=http://localhost:8000/api -f apps/web/Dockerfile -t fintrack-web . -# -# docker stop fintrack-web; docker rm fintrack-web -# ============================================ \ No newline at end of file diff --git a/apps/web/.env.example b/apps/web/.env.example index f4ee38f..5928fc8 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,18 +1,30 @@ -# Environment -NODE_ENV="development" +# FinTrack Web — environment template. +# Set everything under REQUIRED; OPTIONAL has safe defaults. -# Main API URL +# ═════════════════════════════════════════════════════════════ +# REQUIRED — app won't work correctly without these +# ═════════════════════════════════════════════════════════════ + +# Public URLs — browser-facing values for local (non-Docker) runs. In Docker +# the API URL and Telegram bot id are injected by compose — environment for +# dev, build args for prod (see compose.dev.yaml / compose.yaml). NEXT_PUBLIC_API_URL="http://localhost:8000/api" NEXT_PUBLIC_SITE_ORIGIN="http://localhost:5173" -# NextAuth +# Auth (NextAuth) NEXTAUTH_URL="http://localhost:5173/FinTrack/api/auth" -NEXTAUTH_SECRET="your_nextauth_secret_min_32_chars" +NEXTAUTH_SECRET="your_nextauth_secret_min_32_chars" # min 32 chars -# Google OAuth -GOOGLE_CLIENT_ID="your_google_client_id.apps.googleusercontent.com" +# Auth providers +GOOGLE_CLIENT_ID="your_google_client_id.apps.googleusercontent.com" # must match Google Cloud OAuth client GOOGLE_CLIENT_SECRET="your_google_client_secret" - -# Telegram Login Widget -# Numeric bot id — digits before the ":" in the bot token (e.g. 123456789:ABC -> 123456789) +# Telegram Login Widget — numeric bot id, digits before the ":" in the bot +# token (e.g. 123456789:ABC -> 123456789). NEXT_PUBLIC_TELEGRAM_BOT_ID="your_numeric_bot_id" + +# ═════════════════════════════════════════════════════════════ +# OPTIONAL — safe defaults +# ═════════════════════════════════════════════════════════════ + +# Runtime +NODE_ENV="development" # default: development diff --git a/compose.dev.yaml b/compose.dev.yaml index 0193418..f09c6b8 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -102,10 +102,13 @@ services: - NODE_ENV=development - DX_SERVICE_NAME=api - COREPACK_HOME=/corepack + # Override hosts: localhost (in .env) → internal compose service names. + - DATABASE_URL=postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public + - DIRECT_URL=postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public - REDIS_URL=redis://redis:6379 - MONGO_URL=mongodb://mongo:27017/fintrack env_file: - - apps/api/.env.docker + - apps/api/.env # Apply any pending migrations before starting the dev server. # `migrate deploy` is safe to run repeatedly — it only applies new migrations. command: sh -c "corepack enable && pnpm run prisma:migrate:deploy && pnpm run dev" @@ -143,8 +146,13 @@ services: - types_node_modules:/app/packages/types/node_modules environment: - DX_SERVICE_NAME=runner + # Override hosts: localhost (in .env) → internal compose service names. + - DATABASE_URL=postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public + - DIRECT_URL=postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public + - REDIS_URL=redis://redis:6379 + - MONGO_URL=mongodb://mongo:27017/fintrack env_file: - - apps/api/.env.docker + - apps/api/.env # Interactive shell by default command: bash @@ -162,9 +170,10 @@ services: - NODE_ENV=development - DX_SERVICE_NAME=bot - COREPACK_HOME=/corepack + - API_URL=http://api:8000/api - REDIS_URL=redis://redis:6379 env_file: - - apps/bot/.env.docker + - apps/bot/.env command: sh -c "corepack enable && pnpm run dev" depends_on: redis: @@ -195,7 +204,7 @@ services: - COREPACK_HOME=/corepack - WATCHPACK_POLLING=true env_file: - - apps/web/.env.docker + - apps/web/.env command: sh -c "corepack enable && pnpm run dev" depends_on: init: diff --git a/compose.yaml b/compose.yaml index 696dbae..bc5f4b1 100644 --- a/compose.yaml +++ b/compose.yaml @@ -46,10 +46,15 @@ services: dockerfile: apps/api/Dockerfile restart: "unless-stopped" env_file: - # Copy apps/api/.env.docker.example → apps/api/.env.docker and fill in - # your values before running docker compose up. - - apps/api/.env.docker + # Copy apps/api/.env.example → apps/api/.env and fill in your values + # before running docker compose up. + - apps/api/.env environment: + # NODE_ENV must be set here — .env defaults to development for local dev. + - NODE_ENV=production + # Override hosts: localhost (in .env) → internal compose service names. + - DATABASE_URL=postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public + - DIRECT_URL=postgresql://fintrack:fintrack@postgres:5432/fintrack?schema=public - REDIS_URL=redis://redis:6379 - MONGO_URL=mongodb://mongo:27017/fintrack depends_on: @@ -74,9 +79,11 @@ services: dockerfile: apps/bot/Dockerfile restart: "unless-stopped" env_file: - # Copy apps/bot/.env.example → apps/bot/.env.docker and set TELEGRAM_BOT_TOKEN - - apps/bot/.env.docker + # Copy apps/bot/.env.example → apps/bot/.env and set TELEGRAM_BOT_TOKEN + - apps/bot/.env environment: + - NODE_ENV=production + - API_URL=http://api:8000/api - REDIS_URL=redis://redis:6379 depends_on: api: @@ -96,7 +103,7 @@ services: NEXT_PUBLIC_TELEGRAM_BOT_ID: ${NEXT_PUBLIC_TELEGRAM_BOT_ID:-} restart: "unless-stopped" env_file: - - apps/web/.env.docker + - apps/web/.env depends_on: api: condition: service_healthy diff --git a/package.json b/package.json index 74e2db1..52e7014 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,9 @@ "db:api:test:drop": "psql \"postgresql://postgres:postgres@localhost:5432/postgres\" -c \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='fintrack_test'\" -c \"DROP DATABASE IF EXISTS fintrack_test;\"", "db:api:drop:all": "pnpm run db:api:drop && pnpm run db:api:test:drop && psql \"postgresql://postgres:postgres@localhost:5432/postgres\" -c \"DROP ROLE IF EXISTS fintrack;\"", "db:api:clean": "bash scripts/db-restore.sh -e apps/api/.env --clean", - "db:api:clean:dx": "bash scripts/db-restore.sh -e apps/api/.env.docker --clean", + "db:api:clean:dx": "bash scripts/db-restore.sh -e apps/api/.env --clean", "db:api:clean:test": "bash scripts/db-restore.sh -e apps/api/.env.test --clean", - "db:api:clean:test:dx": "bash scripts/db-restore.sh -e apps/api/.env.test.docker --clean", + "db:api:clean:test:dx": "bash scripts/db-restore.sh -e apps/api/.env.test --clean", "test": "pnpm run test:api && pnpm run test:web && pnpm run test:bot", "test:dx": "bash dx shell api pnpm run test:dx && bash dx shell web pnpm run test && bash dx shell bot pnpm run test", "test:api": "pnpm --filter fintrack-api run test", @@ -101,11 +101,11 @@ "dump:web": "bash scripts/codebase-dump.sh -d apps/web", "dump:bot": "bash scripts/codebase-dump.sh -d apps/bot", "dump:db": "bash scripts/db-dump.sh -e apps/api/.env", - "dump:db:dx": "bash scripts/db-dump.sh -e apps/api/.env.docker", + "dump:db:dx": "bash scripts/db-dump.sh -e apps/api/.env", "restore:db": "bash scripts/db-restore.sh -e apps/api/.env", - "restore:db:dx": "bash scripts/db-restore.sh -e apps/api/.env.docker", + "restore:db:dx": "bash scripts/db-restore.sh -e apps/api/.env", "restore:db:reset": "bash scripts/db-restore.sh -e apps/api/.env --reset", - "restore:db:reset:dx": "bash scripts/db-restore.sh -e apps/api/.env.docker --reset" + "restore:db:reset:dx": "bash scripts/db-restore.sh -e apps/api/.env --reset" }, "lint-staged": { "**/*.{ts,tsx}": [ @@ -141,5 +141,14 @@ "@rollup/rollup-linux-x64-gnu": "^4.62.0", "@tailwindcss/oxide-linux-x64-gnu": "*", "lightningcss-linux-x64-gnu": "*" + }, + "pnpm": { + "overrides": { + "nodemailer@<9.0.1": ">=9.0.1", + "shell-quote@<1.8.4": "1.8.4", + "uuid@<11.1.1": "11.1.1", + "js-yaml@3": "4.2.0", + "postcss@<8.5.15": "8.5.15" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0eeaade..671e900 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,19 +5,11 @@ settings: excludeLinksFromLockfile: false overrides: - '@types/express': 4.17.21 - '@types/express-serve-static-core': 4.19.0 - next: ^16.2.4 - '@prisma/client': ^6.19.3 - prisma: ^6.19.3 - zod: ^4.3.6 - postcss: ^8.5.10 - nodemailer: ^8.0.5 - shell-quote: ^1.8.4 - esbuild: ^0.28.1 - uuid: ^11.1.1 - form-data: ^4.0.6 - js-yaml: ^4.2.0 + nodemailer@<9.0.1: '>=9.0.1' + shell-quote@<1.8.4: 1.8.4 + uuid@<11.1.1: 11.1.1 + js-yaml@3: 4.2.0 + postcss@<8.5.15: 8.5.15 importers: @@ -51,7 +43,7 @@ importers: specifier: ^17.0.7 version: 17.0.7 next: - specifier: ^16.2.4 + specifier: ^16.2.9 version: 16.2.9(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) prettier: specifier: ^3.8.4 @@ -127,8 +119,8 @@ importers: specifier: 9.6.3 version: 9.6.3 nodemailer: - specifier: ^8.0.5 - version: 8.0.11 + specifier: ^9.0.1 + version: 9.0.1 openai: specifier: ^6.42.0 version: 6.43.0(zod@4.4.3) @@ -159,16 +151,16 @@ importers: version: 6.0.0 '@types/cookie-parser': specifier: ^1.4.10 - version: 1.4.10(@types/express@4.17.21) + version: 1.4.10(@types/express@5.0.6) '@types/cors': specifier: ^2.8.19 version: 2.8.19 '@types/express': - specifier: 4.17.21 - version: 4.17.21 + specifier: ^5.0.6 + version: 5.0.6 '@types/express-serve-static-core': - specifier: 4.19.0 - version: 4.19.0 + specifier: 5.1.1 + version: 5.1.1 '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -280,7 +272,7 @@ importers: dependencies: '@auth/prisma-adapter': specifier: ^2.11.1 - version: 2.11.2(@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3))(nodemailer@8.0.11) + version: 2.11.2(@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3))(nodemailer@9.0.1) '@fintrack/types': specifier: workspace:* version: link:../../packages/types @@ -309,11 +301,11 @@ importers: specifier: ^1.9.4 version: 1.9.4 next: - specifier: ^16.2.4 + specifier: ^16.2.9 version: 16.2.9(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-auth: specifier: ^4.24.14 - version: 4.24.14(next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(nodemailer@8.0.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 4.24.14(next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(nodemailer@9.0.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: ^19.2.7 version: 19.2.7 @@ -388,7 +380,7 @@ importers: specifier: ^29.0.2 version: 29.1.1(@noble/hashes@1.8.0) postcss: - specifier: ^8.5.10 + specifier: ^8.5.15 version: 8.5.15 typescript: specifier: ~6.0.3 @@ -480,7 +472,7 @@ packages: peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 - nodemailer: ^8.0.5 + nodemailer: '>=9.0.1' peerDependenciesMeta: '@simplewebauthn/browser': optional: true @@ -492,7 +484,7 @@ packages: '@auth/prisma-adapter@2.11.2': resolution: {integrity: sha512-GyNEUNtrPgDPs0M4xX6F5i7jTsCKwU6BXV9zutctcoo6K1Ud+juckrmQS11uyNgeWsw6sliextHbU/e+8lsizQ==} peerDependencies: - '@prisma/client': ^6.19.3 + '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6' '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} @@ -1818,7 +1810,7 @@ packages: resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} engines: {node: '>=18.18'} peerDependencies: - prisma: ^6.19.3 + prisma: '*' typescript: '>=5.1.0' peerDependenciesMeta: prisma: @@ -2274,7 +2266,7 @@ packages: '@types/cookie-parser@1.4.10': resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} peerDependencies: - '@types/express': 4.17.21 + '@types/express': '*' '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} @@ -2288,11 +2280,11 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@4.19.0': - resolution: {integrity: sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - '@types/express@4.17.21': - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -2459,8 +2451,8 @@ packages: resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.1': - resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} '@vitejs/plugin-react@6.0.2': resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} @@ -4678,8 +4670,8 @@ packages: resolution: {integrity: sha512-YRz6xFDXKUwiXSMMChbrBEWyFktZ1qZXEgeSHQQ3nsy08B4c/xLk6REeutRsIFwkjY/1+ShHnu07DN3JeJguig==} peerDependencies: '@auth/core': 0.34.3 - next: ^16.2.4 - nodemailer: ^8.0.5 + next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 + nodemailer: '>=9.0.1' react: ^17.0.2 || ^18 || ^19 react-dom: ^17.0.2 || ^18 || ^19 peerDependenciesMeta: @@ -4756,8 +4748,8 @@ packages: resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} engines: {node: '>=18'} - nodemailer@8.0.11: - resolution: {integrity: sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==} + nodemailer@9.0.1: + resolution: {integrity: sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==} engines: {node: '>=6.0.0'} nodemon@3.1.14: @@ -4853,7 +4845,7 @@ packages: resolution: {integrity: sha512-wVjioGjbnAZycj5mmkFVxbBxLEp+NkKpdMscCYP9LTbq+nbf1WTMVp+ovmD35jgyco4tldWZJkcqdmlh3O9yHQ==} peerDependencies: ws: ^8.18.0 - zod: ^4.3.6 + zod: ^3.25 || ^4.0 peerDependenciesMeta: ws: optional: true @@ -5902,7 +5894,7 @@ packages: peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.18 - esbuild: ^0.28.1 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 @@ -6126,7 +6118,7 @@ packages: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} peerDependencies: - zod: ^4.3.6 + zod: ^3.25.0 || ^4.0.0 zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -6197,7 +6189,7 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} - '@auth/core@0.41.2(nodemailer@8.0.11)': + '@auth/core@0.41.2(nodemailer@9.0.1)': dependencies: '@panva/hkdf': 1.2.1 jose: 6.2.3 @@ -6205,11 +6197,11 @@ snapshots: preact: 10.24.3 preact-render-to-string: 6.5.11(preact@10.24.3) optionalDependencies: - nodemailer: 8.0.11 + nodemailer: 9.0.1 - '@auth/prisma-adapter@2.11.2(@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3))(nodemailer@8.0.11)': + '@auth/prisma-adapter@2.11.2(@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3))(nodemailer@9.0.1)': dependencies: - '@auth/core': 0.41.2(nodemailer@8.0.11) + '@auth/core': 0.41.2(nodemailer@9.0.1) '@prisma/client': 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3) transitivePeerDependencies: - '@simplewebauthn/browser' @@ -8137,9 +8129,9 @@ snapshots: dependencies: '@types/node': 24.13.2 - '@types/cookie-parser@1.4.10(@types/express@4.17.21)': + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': dependencies: - '@types/express': 4.17.21 + '@types/express': 5.0.6 '@types/cookiejar@2.1.5': {} @@ -8151,18 +8143,17 @@ snapshots: '@types/estree@1.0.9': {} - '@types/express-serve-static-core@4.19.0': + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 24.13.2 '@types/qs': 6.15.1 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 - '@types/express@4.17.21': + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.0 - '@types/qs': 6.15.1 + '@types/express-serve-static-core': 5.1.1 '@types/serve-static': 2.2.0 '@types/geojson@7946.0.16': {} @@ -8264,7 +8255,7 @@ snapshots: '@types/swagger-ui-express@4.1.8': dependencies: - '@types/express': 4.17.21 + '@types/express': 5.0.6 '@types/serve-static': 2.2.0 '@types/unist@2.0.11': {} @@ -8372,7 +8363,7 @@ snapshots: '@typescript-eslint/types': 8.61.1 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.1': + '@ungap/structured-clone@1.3.2': optional: true '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': @@ -10549,7 +10540,7 @@ snapshots: jest-worker@30.4.1: dependencies: '@types/node': 24.13.2 - '@ungap/structured-clone': 1.3.1 + '@ungap/structured-clone': 1.3.2 jest-util: 30.4.1 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11057,7 +11048,7 @@ snapshots: neotraverse@0.6.18: {} - next-auth@4.24.14(next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(nodemailer@8.0.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next-auth@4.24.14(next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(nodemailer@9.0.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@babel/runtime': 7.29.7 '@panva/hkdf': 1.2.1 @@ -11072,7 +11063,7 @@ snapshots: react-dom: 19.2.7(react@19.2.7) uuid: 11.1.1 optionalDependencies: - nodemailer: 8.0.11 + nodemailer: 9.0.1 next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: @@ -11138,7 +11129,7 @@ snapshots: node-releases@2.0.47: {} - nodemailer@8.0.11: {} + nodemailer@9.0.1: {} nodemon@3.1.14: dependencies: diff --git a/scripts/db-dump.sh b/scripts/db-dump.sh index f55dc9f..b8ae606 100644 --- a/scripts/db-dump.sh +++ b/scripts/db-dump.sh @@ -7,7 +7,7 @@ # Usage: # bash scripts/db-dump.sh (default .env) # bash scripts/db-dump.sh -e apps/api/.env (custom .env) -# bash scripts/db-dump.sh -e apps/api/.env.docker (custom .env.docker) +# bash scripts/db-dump.sh -e apps/api/.env.test (custom env file) # ================================================================ ENV_FILE=".env" diff --git a/scripts/db-restore.sh b/scripts/db-restore.sh index d037f05..b04bdf1 100644 --- a/scripts/db-restore.sh +++ b/scripts/db-restore.sh @@ -7,7 +7,7 @@ # Usage: # bash scripts/db-restore.sh (latest dump, default .env) # bash scripts/db-restore.sh -f dumps/db/backup-2026-04-09.sql (specific dump) -# bash scripts/db-restore.sh -e apps/api/.env.docker (custom .env.docker) +# bash scripts/db-restore.sh -e apps/api/.env.test (custom env file) # bash scripts/db-restore.sh -r (drop & restore) # bash scripts/db-restore.sh -c|--clean (only drop public objects) # bash scripts/db-restore.sh -f dumps/db/backup.sql -e apps/api/.env -r (full options) diff --git a/scripts/test-env.cjs b/scripts/test-env.cjs new file mode 100644 index 0000000..451dfab --- /dev/null +++ b/scripts/test-env.cjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +// ================================================================ +// test-env.cjs — single host-normalization point for API tests. +// +// Loads apps/api/.env.test (cwd is the api package when run via pnpm), +// then, when running inside Docker, rewrites DB/Mongo hosts from +// localhost → the compose service names. This mirrors resolve_db_url() +// in scripts/db-restore.sh / db-dump.sh, so there is ONE place that +// knows "localhost ↔ container host" — no duplicate .env.docker files. +// +// Usage (from apps/api): +// node ../../scripts/test-env.cjs [args...] +// ================================================================ + +const { existsSync, readFileSync } = require("node:fs"); +const { resolve } = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const ENV_FILE = resolve(process.cwd(), ".env.test"); +if (!existsSync(ENV_FILE)) { + console.error(`❌ Test env file not found: ${ENV_FILE}`); + process.exit(1); +} + +for (const line of readFileSync(ENV_FILE, "utf8").split("\n")) { + const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); + if (!match) continue; + const value = match[2] + .replace(/\s+#.*$/, "") + .trim() + .replace(/^["']|["']$/g, ""); + process.env[match[1]] = value; // override — test env wins +} + +// Inside a container the .env.test localhost hosts are unreachable; rewrite +// them to the compose service names. On the host this block is skipped. +if (existsSync("/.dockerenv")) { + for (const key of ["DATABASE_URL", "DIRECT_URL", "MONGO_URL"]) { + const url = process.env[key]; + if (!url) continue; + process.env[key] = url + .replace("@localhost", "@postgres") + .replace("@127.0.0.1", "@postgres") + .replace("mongodb://localhost", "mongodb://mongo") + .replace("mongodb://127.0.0.1", "mongodb://mongo"); + } +} + +const [command, ...args] = process.argv.slice(2); +if (!command) { + console.error("Usage: node scripts/test-env.cjs [args...]"); + process.exit(1); +} + +const result = spawnSync(command, args, { + stdio: "inherit", + env: process.env, + shell: process.platform === "win32", +}); +process.exit(result.status ?? 1);