From c536f64fa01fbd3fd5d35a2e3c14b3b6291ee9df Mon Sep 17 00:00:00 2001 From: Fab the Analyst Date: Fri, 21 Aug 2026 00:02:15 +0000 Subject: [PATCH] feat: add local docker compose development stack Provide a one-command local stack (API, wallet-provisioning worker, PostgreSQL, Redis) with health checks, dependency startup, explicit migrations + deterministic seed, project-scoped volumes, and a smoke test. Fixes blocked local development: - prisma.config.ts: fall back to a local DATABASE_URL so `prisma generate` succeeds without a live database (also unblocks the Docker build), and move the seed command into the Prisma 7 config - package.json: add predev/prebuild prisma generate hooks, db:generate, db:deploy, worker:dev, and stack:* scripts - src/config/jwt.ts: import JsonWebTokenError via the jsonwebtoken default export so the server boots under real Node ESM - reorder the learner-preferences migration after first_migration (it referenced `users` before that table existed, failing fresh DBs) - Dockerfile: point the healthcheck at /health/live instead of the 404ing /health path; drop accidentally committed UTF-16 error logs - CI: validate `docker compose config` on every PR Closes #125 --- .env.example | 17 ++- .github/workflows/ci.yml | 3 + Dockerfile | 2 +- README.md | 13 ++ docker-compose.yml | 122 ++++++++++++++++++ docker/Dockerfile.dev | 33 +++++ docker/entrypoint-dev-api.sh | 25 ++++ docker/entrypoint-dev-worker.sh | 18 +++ docs/DEVELOPMENT_STACK.md | 104 +++++++++++++++ lint_output.json | Bin 352000 -> 0 bytes lint_results.txt | Bin 1134 -> 0 bytes lint_results_manual.txt | Bin 3214 -> 0 bytes lint_results_utf8.txt | 16 --- package.json | 13 +- prisma.config.ts | 14 +- .../migration.sql | 0 scripts/stack-smoke-test.sh | 60 +++++++++ server_error.txt | Bin 2014 -> 0 bytes server_error_2.txt | Bin 3188 -> 0 bytes server_error_2_utf8.txt | 86 ------------ server_error_utf8.txt | 50 ------- src/config/jwt.ts | 4 +- src/workers/wallet-provisioning.worker.ts | 58 +++++++++ 23 files changed, 478 insertions(+), 160 deletions(-) create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile.dev create mode 100755 docker/entrypoint-dev-api.sh create mode 100755 docker/entrypoint-dev-worker.sh create mode 100644 docs/DEVELOPMENT_STACK.md delete mode 100644 lint_output.json delete mode 100644 lint_results.txt delete mode 100644 lint_results_manual.txt delete mode 100644 lint_results_utf8.txt rename prisma/migrations/{20260718000000_add_learner_preferences => 20260719100043_add_learner_preferences}/migration.sql (100%) create mode 100755 scripts/stack-smoke-test.sh delete mode 100644 server_error.txt delete mode 100644 server_error_2.txt delete mode 100644 server_error_2_utf8.txt delete mode 100644 server_error_utf8.txt create mode 100644 src/workers/wallet-provisioning.worker.ts diff --git a/.env.example b/.env.example index 4250ddb..80e660d 100644 --- a/.env.example +++ b/.env.example @@ -18,9 +18,24 @@ JWT_PREVIOUS_KEYS= # upgrades new hashes immediately; existing hashes upgrade lazily on next login. BCRYPT_SALT_ROUNDS=12 -# Database Configuration +# Database Configuration (local / non-Docker development) DATABASE_URL="postgresql://username:password@localhost:5432/learnault_db?schema=public" +# Docker Compose development stack (docker-compose.yml) +# The API/worker containers build their DATABASE_URL from these values and +# reach PostgreSQL at the `db` service host — no host networking needed. +POSTGRES_USER=learnault +POSTGRES_PASSWORD=learnault +POSTGRES_DB=learnault_dev +# Host ports exposed by the stack (change if 5432/6379/5000 are taken) +POSTGRES_PORT=5432 +REDIS_PORT=6379 +API_PORT=5000 +# Redis is provisioned for upcoming queue-backed work; not yet consumed by the app +REDIS_URL=redis://localhost:6379 +# Wallet-provisioning worker poll interval (ms) +WORKER_POLL_INTERVAL_MS=5000 + # Logging Configuration # LOG_LEVEL=info (options: error, warn, info, http, verbose, debug, silly) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18f9861..43105a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,9 @@ jobs: - name: Lint (ESLint) run: pnpm run lint + - name: Validate Docker Compose stack + run: docker compose config --quiet + - name: Run tests with coverage run: pnpm run test:coverage diff --git a/Dockerfile b/Dockerfile index d2ad7db..db4dc82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -98,6 +98,6 @@ USER appuser EXPOSE 5000 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ - CMD node -e "fetch('http://localhost:5000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + CMD node -e "fetch('http://localhost:5000/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ENTRYPOINT ["./entrypoint-api.sh"] diff --git a/README.md b/README.md index 1e38e66..3f542e6 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,19 @@ pnpm db:seed pnpm dev ``` +### Run the full stack with Docker Compose (recommended) + +The local stack — API, wallet worker, PostgreSQL, and Redis — starts with one command: + +```bash +cp .env.example .env +docker compose up -d --build +``` + +Migrations and deterministic seed fixtures run automatically on boot. See +[Local Development Stack](./docs/DEVELOPMENT_STACK.md) for health checks, logs, +reset, and the smoke test (`pnpm stack:smoke`). + For detailed database setup instructions, see [Prisma Setup Guide](./prisma/SETUP.md) ### Development Workflow diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8921f69 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,122 @@ +# ============================================================================ +# Learnault API — Local Development Stack +# +# One command starts a healthy stack (API + wallet worker + PostgreSQL + Redis): +# docker compose up -d --build +# +# The API container applies migrations and seeds deterministic fixtures on +# boot (see docker/entrypoint-dev-api.sh). The worker drains the idempotent +# wallet-provisioning outbox (see src/workers/wallet-provisioning.worker.ts). +# +# Useful commands: +# docker compose ps → service status + health +# docker compose logs -f → follow logs (all services) +# docker compose down → stop the stack (keeps data volumes) +# docker compose down -v → stop and delete data volumes (project-scoped reset) +# pnpm stack:smoke → validate config + run the smoke test +# +# Docs: docs/DEVELOPMENT_STACK.md +# ============================================================================ + +name: learnault-dev + +services: + # -------------------------------------------------------------------------- + # PostgreSQL — primary datastore + # -------------------------------------------------------------------------- + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-learnault} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-learnault} + POSTGRES_DB: ${POSTGRES_DB:-learnault_dev} + ports: + - '${POSTGRES_PORT:-5432}:5432' + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-learnault} -d ${POSTGRES_DB:-learnault_dev}'] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + + # -------------------------------------------------------------------------- + # Redis — cache/queue (reserved for upcoming queue-backed work) + # -------------------------------------------------------------------------- + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - '${REDIS_PORT:-6379}:6379' + volumes: + - redisdata:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 5s + retries: 10 + + # -------------------------------------------------------------------------- + # API — Express server (nodemon, hot reload) + # -------------------------------------------------------------------------- + api: + build: + context: . + dockerfile: docker/Dockerfile.dev + restart: unless-stopped + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + environment: + NODE_ENV: development + PORT: 5000 + DATABASE_URL: postgresql://${POSTGRES_USER:-learnault}:${POSTGRES_PASSWORD:-learnault}@db:5432/${POSTGRES_DB:-learnault_dev}?schema=public + REDIS_URL: redis://redis:6379 + JWT_SECRET: ${JWT_SECRET:-dev-only-secret-change-me} + JWT_ISSUER: ${JWT_ISSUER:-learnault-api} + JWT_AUDIENCE: ${JWT_AUDIENCE:-learnault-clients} + RUN_MIGRATIONS: 'true' + RUN_SEED: 'true' + ports: + - '${API_PORT:-5000}:5000' + volumes: + # Bind-mount source for hot reload; keep the image's node_modules + - .:/app + - /app/node_modules + healthcheck: + test: ['CMD', 'node', '-e', "fetch('http://localhost:5000/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + stop_grace_period: 30s + + # -------------------------------------------------------------------------- + # Worker — drains the wallet-provisioning outbox + # -------------------------------------------------------------------------- + worker: + build: + context: . + dockerfile: docker/Dockerfile.dev + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + NODE_ENV: development + DATABASE_URL: postgresql://${POSTGRES_USER:-learnault}:${POSTGRES_PASSWORD:-learnault}@db:5432/${POSTGRES_DB:-learnault_dev}?schema=public + RUN_MIGRATIONS: 'true' + WORKER_POLL_INTERVAL_MS: ${WORKER_POLL_INTERVAL_MS:-5000} + volumes: + - .:/app + - /app/node_modules + command: ['./docker/entrypoint-dev-worker.sh'] + stop_grace_period: 30s + +volumes: + pgdata: + redisdata: diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 0000000..b13a45f --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,33 @@ +# ============================================================================ +# Learnault API — Development Docker Image +# +# Used by docker-compose.yml for local development. Installs all dependencies +# (including dev tooling) and generates the Prisma client. The source tree is +# bind-mounted from the host at runtime so edits hot-reload via nodemon/tsx. +# +# Build: +# docker build -f docker/Dockerfile.dev -t learnault-api:dev . +# ============================================================================ + +FROM node:20-slim + +# OpenSSL is required by Prisma's engine detection +RUN apt-get update -y && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +RUN corepack enable && corepack prepare pnpm@10 --activate + +WORKDIR /app + +# Install ALL dependencies (dev tooling included) in a cached layer +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# Generate the Prisma client (needed before the app or worker can boot) +COPY prisma ./prisma +COPY prisma.config.ts ./ +RUN npx prisma generate + +EXPOSE 5000 + +# Entrypoints are referenced via the bind-mounted ./docker directory at runtime +CMD ["./docker/entrypoint-dev-api.sh"] diff --git a/docker/entrypoint-dev-api.sh b/docker/entrypoint-dev-api.sh new file mode 100755 index 0000000..cd2ffe7 --- /dev/null +++ b/docker/entrypoint-dev-api.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +# --------------------------------------------------------------------------- +# Learnault API — Development Container Entrypoint +# +# Environment variables: +# RUN_MIGRATIONS = "true" (default) → apply pending migrations on boot +# RUN_SEED = "true" (default) → seed deterministic fixtures on boot +# --------------------------------------------------------------------------- + +if [ "${RUN_MIGRATIONS:-true}" = "true" ]; then + echo "[entrypoint] Applying database migrations …" + npx prisma migrate deploy + echo "[entrypoint] Migrations applied." +fi + +if [ "${RUN_SEED:-true}" = "true" ]; then + echo "[entrypoint] Seeding database (deterministic fixtures) …" + npx prisma db seed + echo "[entrypoint] Seed complete." +fi + +echo "[entrypoint] Starting API dev server (nodemon) …" +exec pnpm dev diff --git a/docker/entrypoint-dev-worker.sh b/docker/entrypoint-dev-worker.sh new file mode 100755 index 0000000..3fb8fcd --- /dev/null +++ b/docker/entrypoint-dev-worker.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# --------------------------------------------------------------------------- +# Learnault Worker — Development Container Entrypoint +# +# Environment variables: +# RUN_MIGRATIONS = "true" (default) → apply pending migrations on boot +# --------------------------------------------------------------------------- + +if [ "${RUN_MIGRATIONS:-true}" = "true" ]; then + echo "[entrypoint] Applying database migrations …" + npx prisma migrate deploy + echo "[entrypoint] Migrations applied." +fi + +echo "[entrypoint] Starting wallet-provisioning worker …" +exec pnpm worker:dev diff --git a/docs/DEVELOPMENT_STACK.md b/docs/DEVELOPMENT_STACK.md new file mode 100644 index 0000000..2f29347 --- /dev/null +++ b/docs/DEVELOPMENT_STACK.md @@ -0,0 +1,104 @@ +# Local Development Stack (Docker Compose) + +A reproducible local stack for the Learnault API: **API**, **wallet worker**, **PostgreSQL**, and **Redis** — started with one command. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) with Docker Compose v2 (bundled with Docker Desktop) +- Node.js 20+ and pnpm 10+ (only needed for `pnpm` helper scripts; the stack itself is containerized) + +## Quick Start + +```bash +# 1. Configure environment (create once; defaults work out of the box) +cp .env.example .env + +# 2. One command builds and starts a healthy stack +docker compose up -d --build + +# 3. Verify everything is healthy +docker compose ps +# NAME STATUS +# learnault-dev-api Up ... (healthy) +# learnault-dev-db Up ... (healthy) +# learnault-dev-redis Up ... (healthy) +# learnault-dev-worker Up ... (healthy) +``` + +The API is available at `http://localhost:5000` (Swagger UI at `http://localhost:5000/api-docs`). + +## What happens on startup + +The `api` service entrypoint (`docker/entrypoint-dev-api.sh`) waits for PostgreSQL and Redis health, then: + +1. Applies pending migrations (`prisma migrate deploy`) — deterministic, no-op when up to date. +2. Seeds deterministic fixtures (`prisma db seed`) — idempotent, safe to run repeatedly. +3. Starts the Express server under `nodemon`, so source edits hot-reload via the bind mount. + +The `worker` service runs `src/workers/wallet-provisioning.worker.ts`, which polls the idempotent wallet-provisioning outbox and generates Stellar keys through the dev in-memory KMS adapter. In production, swap the KMS adapter for a real one (e.g. AWS KMS) behind the same `KmsSecretStore` interface. + +## Health checks & readiness + +| Endpoint | Meaning | +| ------------------- | ---------------------------------------------------- | +| `GET /health/live` | Process is alive (used by the container healthcheck) | +| `GET /health/ready` | Dependencies (database) are reachable | + +The API container only reports **healthy** after `/health/live` responds; `depends_on: condition: service_healthy` keeps the worker from racing migrations. `GET /health/ready` returns `200` only when PostgreSQL is reachable — the smoke test waits on it. + +## One-command helpers + +`package.json` exposes convenient wrappers: + +```bash +pnpm stack:up # docker compose up -d --build +pnpm stack:down # stop the stack (keeps data volumes) +pnpm stack:reset # stop + delete data volumes (project-scoped reset) +pnpm stack:logs # follow API + worker logs +pnpm stack:validate # docker compose config --quiet +pnpm stack:smoke # validate + start + probe health endpoints +``` + +## Logs & graceful shutdown + +```bash +docker compose logs -f # all services +docker compose logs -f api # API only +docker compose logs worker # worker only +``` + +Both services have `stop_grace_period: 30s`, matching the app's graceful-shutdown handler (`SHUTDOWN_TIMEOUT_MS`): `docker compose down` sends `SIGTERM`, the server drains HTTP connections and closes the Prisma pool before exiting. + +## Data persistence & reset + +- PostgreSQL data lives in the `learnault-dev_pgdata` named volume; Redis in `learnault-dev_redisdata`. +- **Reset is project-scoped**: `docker compose down -v` removes only this project's volumes. Other projects and containers are untouched. + +```bash +# Full project-scoped reset (drops all local data, then rebuild + reseed) +pnpm stack:reset +pnpm stack:up +``` + +## Smoke test + +```bash +pnpm stack:smoke +``` + +This validates the compose file, starts the stack, waits for `/health/ready`, probes `/health/live` and `/health/ready`, and prints service status. Run `./scripts/stack-smoke-test.sh --validate` for config-only validation. + +## Troubleshooting + +| Symptom | Fix | +| ------------------------------------ | -------------------------------------------------------------------- | +| Port 5432/6379/5000 already in use | Override in `.env`: `POSTGRES_PORT=5433`, `REDIS_PORT=6380`, `API_PORT=5001` | +| Prisma client errors (`@prisma/client` export) | Run `pnpm db:generate` (or `docker compose build`), then restart the stack | +| `JWT_SECRET` required error | Set a real `JWT_SECRET` in `.env` (defaults are dev-only) | +| Containers restarting after reset | Ensure `.env` exists before `docker compose up` | + +## Related + +- [Prisma Setup Guide](../prisma/SETUP.md) — database schema, migrations, seeding +- [Staging Runbook](./RUNBOOK.md) — production/staging deployment +- [Architecture](./ARCHITECTURE.md) — service design diff --git a/lint_output.json b/lint_output.json deleted file mode 100644 index c716141d1698bf5d80c21b5d5d3a5d83627c4947..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 352000 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�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 diff --git a/lint_results.txt b/lint_results.txt deleted file mode 100644 index bdac08d3cf830d9f95b1057fe2350af7974c4660..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1134 zcmcJP%SyyR5JfMz6a0tXso?lV(S<1L!i7r_bbQXlG3Y!sEDjRo>qwG_DN=-73+AGWB>5(F!$?21>Q5dCh8pH6L*)m_n6bq8xN-3t()o z*0$Ew)B%VD<{Wuf`#f7nJJ@pFz|<#7vlhQD|IDR{Po@_57JfZl5xIkw>z-^oJdQLA zYaX)*WE{wSUCX>J`+J6O7PVbHfX(m?cqHn=XXe|;ZhU3+nXE5q6}H7F{{`Dx%xx+1 z{U$f(_y6^Fty~>v=Je-SpyU}W< zZj9N|HS(NJwWniP8amR6p3u**4*3?Ck3x@R?tX5;lOlV1>XR`?ygs&i+{RbjYF?hl zUi1p%GHU=+pL4m=-I&`Hy;twJTr=qI>d{(Ge|7#(eBeyIST9fVsFzPaAfjvU`A)&Q lW{=oCWe@0OHug+GxH5hx?B|@V@rgS}_e`6^Gy45c_yEkvz=Z$+ diff --git a/lint_results_manual.txt b/lint_results_manual.txt deleted file mode 100644 index 99c6fb74d511930e1e7be4ebbb9f915488293618..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3214 zcmds(+iuf95QgU(iFXJ$ChDQg!e>Ns* zi2@Qu(8^lx?(EF$%s&VF+tL9 z-Uo2xMB)L`ht8cK-Je=rv(ISBplGF=GSCrk%j#~abnGGd#wAL(AhK0rsj>Eh=Lq|S z)653hI(ADH`4OuLIgr>9BV<7eRhcD}Ys97o!ejPbm%5IXyk4%c0Vc|`6s$s|Lxd7` z17z#q)w0;$b8mvTBi3zu1xEYe8#Aj0UT_l7jad(|W{h+|NfJFj7pAfMqe{uRA>{lD zs~q{*{pum78W8M#S}JRCkF-L2M?(KwYY!wmL+z@0t9ZwJIm$T=_M|!tI68na{okQNASeq)q+# zcGSf3=n1)hQxhAu;n7=Ni2DJRcLv_-W>o}3t=$FG!?$}`ms{M4u2ScBRi}yQG+RVe zbJQ3UeYulc+aJ^uZswa=A-YaZs#mZ75>M5*vh%KFqcTx={!TWYxaU{@YAOUH@C*I2 H`iuAum^3s0 diff --git a/lint_results_utf8.txt b/lint_results_utf8.txt deleted file mode 100644 index 8916d4a..0000000 --- a/lint_results_utf8.txt +++ /dev/null @@ -1,16 +0,0 @@ - -> learnault-api@0.1.0 lint -> eslint . - - -C:\Users\EMMA\Desktop\learn\learnault-api\src\config\swagger.ts - 1:41 error Extra semicolon semi - 37:2 error Extra semicolon semi - 39:43 error Extra semicolon semi - -C:\Users\EMMA\Desktop\learn\learnault-api\src\controllers\user.controller.ts - 1:62 warning 'UpdateWalletData' is defined but never used. Allowed unused vars must match /^I[A-Z]|^_/u @typescript-eslint/no-unused-vars - -Γ£û 4 problems (3 errors, 1 warning) - 3 errors and 0 warnings potentially fixable with the `--fix` option. - diff --git a/package.json b/package.json index f005d32..a441a58 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "license": "MIT", "author": "Learnault Contributors", "scripts": { + "predev": "prisma generate", "dev": "nodemon", + "prebuild": "prisma generate", "build": "tsc", "start": "node dist/server.js", "lint": "eslint .", @@ -29,11 +31,20 @@ "test:ci": "vitest run", "test:integration": "vitest run --reporter=verbose tests/integration/", "test:integration:watch": "vitest tests/integration/", + "db:generate": "prisma generate", "db:migrate": "prisma migrate dev", + "db:deploy": "prisma migrate deploy", "seed": "tsx prisma/seed.ts", "seed:reset": "tsx prisma/seed.ts --reset", "db:seed": "npm run seed", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "worker:dev": "tsx src/workers/wallet-provisioning.worker.ts", + "stack:validate": "docker compose config --quiet", + "stack:up": "docker compose up -d --build", + "stack:down": "docker compose down", + "stack:reset": "docker compose down -v", + "stack:logs": "docker compose logs -f api worker", + "stack:smoke": "bash scripts/stack-smoke-test.sh" }, "dependencies": { "@prisma/adapter-pg": "^7.4.2", diff --git a/prisma.config.ts b/prisma.config.ts index 4ddab3a..0dd9333 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -1,12 +1,20 @@ import 'dotenv/config' -import { defineConfig, env } from 'prisma/config' +import { defineConfig } from 'prisma/config' + +// `prisma generate` (and the Docker build) must succeed even when no database +// is reachable, so fall back to a local default when DATABASE_URL is unset. +// `prisma migrate`/`db push` still require a real DATABASE_URL. +const DATABASE_URL = + process.env.DATABASE_URL ?? + 'postgresql://postgres:postgres@localhost:5432/learnault_dev?schema=public' export default defineConfig({ schema: 'prisma/schema.prisma', migrations: { path: 'prisma/migrations', + seed: 'tsx prisma/seed.ts', }, datasource: { - url: env('DATABASE_URL'), + url: DATABASE_URL, }, -}) \ No newline at end of file +}) diff --git a/prisma/migrations/20260718000000_add_learner_preferences/migration.sql b/prisma/migrations/20260719100043_add_learner_preferences/migration.sql similarity index 100% rename from prisma/migrations/20260718000000_add_learner_preferences/migration.sql rename to prisma/migrations/20260719100043_add_learner_preferences/migration.sql diff --git a/scripts/stack-smoke-test.sh b/scripts/stack-smoke-test.sh new file mode 100755 index 0000000..9bf34c2 --- /dev/null +++ b/scripts/stack-smoke-test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------------------------------------- +# Learnault API — Local Stack Smoke Test +# +# Validates docker-compose.yml, starts the stack, and verifies the API is +# live and ready. Used by `pnpm stack:smoke` and by CI. +# +# Usage: +# ./scripts/stack-smoke-test.sh # validate + start + probe +# ./scripts/stack-smoke-test.sh --validate # config validation only +# --------------------------------------------------------------------------- + +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" +API_URL="${API_URL:-http://localhost:5000}" +MAX_RETRIES=60 +RETRY_INTERVAL=2 + +echo "==> Validating compose configuration ($COMPOSE_FILE)" +docker compose -f "$COMPOSE_FILE" config --quiet +echo "==> Compose configuration is valid" + +if [ "${1:-}" = "--validate" ]; then + exit 0 +fi + +echo "==> Building and starting the stack" +docker compose -f "$COMPOSE_FILE" up -d --build + +echo "==> Waiting for dependencies and API readiness ($API_URL/health/ready)" +ready=false +for ((i = 1; i <= MAX_RETRIES; i++)); do + if curl -fsS "$API_URL/health/ready" >/dev/null 2>&1; then + ready=true + break + fi + echo " ... attempt $i/$MAX_RETRIES" + sleep "$RETRY_INTERVAL" +done + +if [ "$ready" != "true" ]; then + echo "!! API did not become ready in time" >&2 + docker compose -f "$COMPOSE_FILE" logs api + exit 1 +fi + +echo "==> Liveness probe (GET $API_URL/health/live)" +curl -fsS "$API_URL/health/live" +echo + +echo "==> Readiness probe (GET $API_URL/health/ready)" +curl -fsS "$API_URL/health/ready" +echo + +echo "==> Service status" +docker compose -f "$COMPOSE_FILE" ps + +echo "" +echo "✅ Smoke test passed" diff --git a/server_error.txt b/server_error.txt deleted file mode 100644 index 937985c55d857487f1add0c27e1f7a02fb7fbd66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2014 zcmbW2TW`}q5QXO%iT_~rfwThB8!aeLRBe!gNChc~2MUp!IFOP!Au*5$!A}RiGixih zqS8{WZ0}_*=bW8g|M{KT#QK(4ooB%lS%c@8_qn~XIWsff=hn0~?XKOiBSt68&+W5i zcuyIR7{^$qm40kr-D8B^L)ND@WPOfZk8cm()CSCbWYlLpk_%{O%a6h*NvMzZf-5h+k40}%9~IgJIe%K6LxKBE!Yj{ z7dDaST8)aHYUmWZ%+63FVV{P+(^@ZbBxoA2!+R|Rpi@hRM#p2T> z*NS1pNV>IH3Fqq6^&C=_39Kg|2pEbSG5JE2;2Gxi9%4E5XgUQycwRcMIrA~GbS@IS zwJQ6cdrC5)_+;_zatLv~t*iSr0h^lidR>D6)xgqO{C9NWQi;^xvk+L$*v)8UvsmfV* zPsr9T;eAg3f|~1+Gj&eNsk)GT!Do}XaECVBPAj?zRT9Z9b3qMHNJIh!DEGQ-}db>_ztc`r_dHj#UiF*WvN5qM}US=RZq#d}jhu30XsoZfb zn_v(U-6l(rPx8Hf%Qxc;G&v(}dr``F8|*XBp}p&(OYAvxkL^|z7Y^O7;oqhIS;VtcIk)<=)PosW)t?4#G$=%*_V!%j4l*~BWCOk>;daJn37dy zckCR>36cu&mVcD}X|?~&bu;y$ZA+qI{lcHkg8GN%|}{XRSQ*&iX{ zE@R*SWer`0_)y=beuIX71D8>v|2cf-ctToF;E}U_#(QDE)T}$iHpxi#b=fb8LJz&} z`pyA=I_`l2bRWAq@P6)o*GlL+{QHb?tBPP@S4iBk>zXYu;JfWn5TYLYTQ%f*>=kz* z4)FAh49L(&J~}0e8B!+jeCBc#y^t_=L~cVkcl}9P{%vg^A==Ug-Nz$@>{O3@dFWQ= z*fk|qOFPEKr_2*g$L(9K(oARQA#@9SjIVOyGyykpT#}LFd(mxeJZ%S+oIvQ*g%5Qa zlrQVKlH?1ayy+u<;aR5Gd=9_g?GO8c?`nTLw|w#g zxqQL1v`5S`*F(0+o+D({eKL2y9kUa4<)Qm?LRRKjFhzsVA<4*)7hD1v|I===PTvKY zFPdxSIuH7;&XW*LA1QJZw<$FKtI|XkN^2oP=2pYvy392H(=wQ6sNnx&e3&b(kpvA$%bNXqBeZu9dBk?~daEvUwdzGA%eY`Ci8 zD9nFr@^dXSN;K}j_r)2jGheO8qv@F~d*gKS=c+2aB|k%U;-i($oxaVTb$dm( zUE&L!KJC1DFRNpc^o?_T`vKn+;rdsS(^@rem2aBgbb5t|e%ZEH3Q-UPove1fQi$l6 zZF{8<(J$NfN+F_Ow(XTdsH3d+_D#J~2zAB*+1svH3en7`X0KE)+^uq{8Mkl2MgA|z z{wdxr?YmkG#rfvGx9PqU+Sl(t>oH7rMa~MLYN<{+Mj!Ri5kohvWXC?IYKP7r=b>rC z33`t`PjdT_`3O|pE|iN=t>Z@5M~XbF{8w&W(36`tm~Hk-KLP63^}V6JaNd3U3Qcy= zr?ODtv))V`wChuc(kJ=`A zIx(5cQ2yTI-1Xh#om#D|-+zB~8%q%Kdg>sz!KGXAZ7RDwqx_LSqSvVF%0EI#uI9=| s)$DN%F=rjND?fF=zZoG3QAIV=oktwIJOBUy diff --git a/server_error_2_utf8.txt b/server_error_2_utf8.txt deleted file mode 100644 index 15f50c1..0000000 --- a/server_error_2_utf8.txt +++ /dev/null @@ -1,86 +0,0 @@ -node.exe : C:\Users\EMM -A\Desktop\learn\learnau -lt-api\node_modules\.pn -pm\@prisma+client@7.4.2 -_prisma_49b4b128965f74e -a9bbd7586bc0c7d7a\node_ -modules\@prisma\client\ -src\runtime\getPrismaCl -ient.ts:260 -At line:1 char:1 -+ & "C:\nvm4w\nodejs/no -de.exe" "C:\Users\EMMA\ -AppData\Roaming\npm/nod -e_ ... -+ ~~~~~~~~~~~~~~~~~~~~~ -~~~~~~~~~~~~~~~~~~~~~~~ -~~~~~~~~~~~~~~~~~~~~~~~ -~~ - + CategoryInfo - : NotSpecifi - ed: (C:\Users\EMMA - \D...maClient.ts:2 -60:String) [], Rem -oteException - + FullyQualifiedEr - rorId : NativeComm - andError - - throw new Prism -aClientInitializationEr -ror( - ^ - - -PrismaClientInitializat -ionError: -`PrismaClient` needs -to be constructed with -a non-empty, valid -`PrismaClientOptions`: - -``` -new PrismaClient({ - ... -}) -``` - -or - -``` -constructor() { - super({ ... }); -} -``` - - at new t (C:\Users\ -EMMA\Desktop\learn\lear -nault-api\node_modules\ -.pnpm\@prisma+client@7. -4.2_prisma_49b4b128965f -74ea9bbd7586bc0c7d7a\no -de_modules\@prisma\clie -nt\src\runtime\getPrism -aClient.ts:260:15) - at (C:\ -Users\EMMA\Desktop\lear -n\learnault-api\src\con -fig\database.ts:7:42) - at ModuleJob.run (n -ode:internal/modules/es -m/module_job:345:25) - at async onImport.t -racePromise.__proto__ ( -node:internal/modules/e -sm/loader:665:26) - at async asyncRunEn -tryPointWithESMLoader ( -node:internal/modules/r -un_main:117:5) { - clientVersion: -'7.4.2', - errorCode: undefined, - retryable: undefined -} - -Node.js v22.20.0 diff --git a/server_error_utf8.txt b/server_error_utf8.txt deleted file mode 100644 index cbd4b5f..0000000 --- a/server_error_utf8.txt +++ /dev/null @@ -1,50 +0,0 @@ -node.exe : C:\Users\EMM -A\Desktop\learn\learnau -lt-api\src\config\datab -ase.ts:1 -At line:1 char:1 -+ & "C:\nvm4w\nodejs/no -de.exe" "C:\Users\EMMA\ -AppData\Roaming\npm/nod -e_ ... -+ ~~~~~~~~~~~~~~~~~~~~~ -~~~~~~~~~~~~~~~~~~~~~~~ -~~~~~~~~~~~~~~~~~~~~~~~ -~~ - + CategoryInfo - : NotSpecifi - ed: (C:\Users\EMMA - \D...g\database.ts -:1:String) [], Rem -oteException - + FullyQualifiedEr - rorId : NativeComm - andError - -import { PrismaClient -} from '@prisma/client' - ^ - -SyntaxError: The -requested module -'@prisma/client' does -not provide an export -named 'PrismaClient' - at -ModuleJob._instantiate -(node:internal/modules/ -esm/module_job:228:21) - at async -ModuleJob.run (node:int -ernal/modules/esm/modul -e_job:337:5) - at async onImport.t -racePromise.__proto__ ( -node:internal/modules/e -sm/loader:665:26) - at async asyncRunEn -tryPointWithESMLoader ( -node:internal/modules/r -un_main:117:5) - -Node.js v22.20.0 diff --git a/src/config/jwt.ts b/src/config/jwt.ts index d69ca22..3262612 100644 --- a/src/config/jwt.ts +++ b/src/config/jwt.ts @@ -1,4 +1,4 @@ -import { Algorithm, JsonWebTokenError, SignOptions, VerifyOptions } from 'jsonwebtoken' +import jwt, { Algorithm, SignOptions, VerifyOptions } from 'jsonwebtoken' import { JWTPayload, signToken, verifyToken } from '../utils/jwt' // Centralized JWT policy: one algorithm, one issuer/audience pair, and an @@ -107,7 +107,7 @@ export function verifyAccessToken(token: string, options: VerifyOptions = {}): A // Same error type jwt.verify() itself throws for a bad signature, so // callers (e.g. authenticate()) treat this as "invalid token" (401) // rather than an unexpected server error (500). - throw new JsonWebTokenError('Unknown or retired signing key') + throw new jwt.JsonWebTokenError('Unknown or retired signing key') } return verifyToken(token, secret, { diff --git a/src/workers/wallet-provisioning.worker.ts b/src/workers/wallet-provisioning.worker.ts new file mode 100644 index 0000000..77fd580 --- /dev/null +++ b/src/workers/wallet-provisioning.worker.ts @@ -0,0 +1,58 @@ +import 'dotenv/config' +import { WalletProvisioningOutboxHandler } from '../jobs/wallet-provisioning.handler' +import { InMemoryEnvelopeKms } from '../services/kms/in-memory-envelope-kms' +import { SdkStellarKeypairGenerator } from '../services/stellar-keypair.adapter' +import { PrismaWalletProvisioningRepository } from '../services/wallet-provisioning.repository' +import prisma from '../config/database' + +const POLL_INTERVAL_MS = parseInt(process.env.WORKER_POLL_INTERVAL_MS ?? '5000', 10) + +// Development worker: drains the idempotent wallet-provisioning outbox using +// the in-memory KMS adapter. Production should swap in a real KMS adapter +// (e.g. AWS KMS) — the handler only depends on the KmsSecretStore interface. +const kms = new InMemoryEnvelopeKms() +const repository = new PrismaWalletProvisioningRepository(prisma) +const keypairGenerator = new SdkStellarKeypairGenerator() +const handler = new WalletProvisioningOutboxHandler(repository, kms, keypairGenerator) + +let isShuttingDown = false + +async function drainOnce(): Promise { + const result = await handler.handleNext() + if (result.kind !== 'idle') { + console.log(`[worker] ${result.kind}:`, JSON.stringify(result)) + } +} + +async function runLoop(): Promise { + console.log(`[worker] Wallet provisioning worker started (poll every ${POLL_INTERVAL_MS}ms)`) + while (!isShuttingDown) { + try { + await drainOnce() + } catch (error) { + console.error('[worker] Unhandled error while draining outbox:', error) + } + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + } +} + +function gracefulShutdown(signal: string): void { + if (isShuttingDown) return + isShuttingDown = true + console.log(`[worker] Received ${signal}, shutting down...`) + prisma + .$disconnect() + .then(() => process.exit(0)) + .catch((error) => { + console.error('[worker] Error disconnecting Prisma:', error) + process.exit(1) + }) +} + +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')) +process.on('SIGINT', () => gracefulShutdown('SIGINT')) + +runLoop().catch((error) => { + console.error('[worker] Fatal error:', error) + process.exit(1) +})