diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..ba812fb1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + containerized-tests: + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Generate test environment file + run: ./generate-env.sh .env.test + + - name: Build test images + run: docker compose --env-file .env.test -f docker-compose.test.yaml build + + - name: Run tests + run: docker compose --env-file .env.test -f docker-compose.test.yaml up --abort-on-container-exit + + - name: Check test container exit codes + if: always() + run: | + status=0 + for name in test-env-api-specs test-env-unit-tests test-env-frontend-tests; do + code="$(docker inspect -f '{{.State.ExitCode}}' "$name" 2>/dev/null || echo 1)" + echo "$name exited with $code" + if [ "$code" != "0" ]; then + status=1 + fi + done + exit $status + + - name: Show service logs on failure + if: failure() + run: docker compose --env-file .env.test -f docker-compose.test.yaml logs + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: web/test-results + + - name: Tear down + if: always() + run: docker compose --env-file .env.test -f docker-compose.test.yaml down -v diff --git a/.gitignore b/.gitignore index 078de8a3..44912412 100644 --- a/.gitignore +++ b/.gitignore @@ -501,3 +501,4 @@ FodyWeavers.xsd *storybook.log storybook-static +web/test-results/frontend/vitest-storybook.xml diff --git a/README.md b/README.md index e96faf3c..17222621 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ prerequisite list, including the pipeline's C++ toolchain. For full setup details, use [Getting Started](docs/website/getting-started.md). This section is the shortest path to a working dev environment. -### Backend (Docker) +### Full stack (Docker) ```bash git clone https://github.com/aaronpo97/the-biergarten-app @@ -94,14 +94,15 @@ cp web/.env.example web/.env.dev docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up --build -d ``` -Backend access: +Access: +- Website: http://localhost:3000 - API Swagger: http://localhost:8080/swagger - Health Check: http://localhost:8080/health - Mailpit UI (dev SMTP): http://localhost:8025 - SeaweedFS S3 API (dev object storage): http://localhost:8333 -### Frontend (Node) +### Frontend (Node, outside Docker) ```bash cd web/frontend @@ -140,14 +141,18 @@ archive/ ## Testing -Create a test environment file, then run the backend test stack with Docker: +Create a test environment file, then run the full test stack (backend and +frontend) with Docker: ```bash cp web/.env.example web/.env.test docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit ``` -See [Testing](docs/website/testing.md) for the full command list. +The same command runs in CI via +[`.github/workflows/tests.yml`](.github/workflows/tests.yml) on every push +and pull request to `main`. See [Testing](docs/website/testing.md) for the +full command list. ## Configuration diff --git a/docs/website/docker.md b/docs/website/docker.md index fba01399..20b882cc 100644 --- a/docs/website/docker.md +++ b/docs/website/docker.md @@ -11,6 +11,7 @@ The project uses Docker Compose to orchestrate multiple services: - Database migrations runner (DbUp) - Database seeder - .NET API +- React Router website (SSR frontend) - Test runners See the [deployment diagram](diagrams-out/deployment.svg) for visual @@ -39,6 +40,7 @@ sqlserver # SQL Server 2022 (port 1434) database.migrations # DbUp migrations database.seed # Seed initial data api.core # Web API (ports 8080, 8081) +frontend # React Router website (port 3000) mailpit # Local dev SMTP server + UI (port 8025) seaweedfs # S3-compatible object storage (ports 8333, 8888, 9333, 23646) ``` @@ -51,6 +53,7 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d **Access**: +- Website: http://localhost:3000 - API Swagger: http://localhost:8080/swagger - Health Check: http://localhost:8080/health - Mailpit UI: http://localhost:8025 @@ -90,6 +93,7 @@ database.migrations # Fresh schema database.seed # Test data api.specs # Reqnroll BDD tests unit.tests # All Features.*.Tests unit test projects +frontend.tests # Storybook Vitest + Playwright suites (own Playwright image) ``` **Run Tests**: @@ -119,13 +123,16 @@ docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v - Health checks enabled - Restart policies (unless-stopped) - Security hardening +- Only `frontend` publishes a port to the host; `sqlserver` and `api.core` + are reachable only from other containers on `prodnet` **Services**: ```yaml -sqlserver # Production SQL Server +sqlserver # Production SQL Server (internal only) database.migrations # Schema updates only -api.core # Production API +api.core # Production API (internal only, no published ports) +frontend # Production website (port 3000, the only public binding) ``` **Deploy Production**: diff --git a/docs/website/environment-variables.md b/docs/website/environment-variables.md index aa5aa630..8b1534b1 100644 --- a/docs/website/environment-variables.md +++ b/docs/website/environment-variables.md @@ -20,7 +20,9 @@ Direct environment variable access via `Environment.GetEnvironmentVariable()`. ### Frontend (`web/frontend`) The active website reads runtime values from the server environment for its auth -and API integration. +and API integration. In Docker, the `frontend` service is wired into the same +`web/.env.dev` / `.env.test` / `.env.prod` files as the backend; running it +locally with `npm run dev` still reads from the shell environment. ### Docker @@ -34,8 +36,9 @@ Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml: `web/generate-env.sh` copies `.env.example` to a target file and replaces the secret-bearing values (`DB_PASSWORD`, `ACCESS_TOKEN_SECRET`, -`REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SMTP_USERNAME`, -`SMTP_PASSWORD`, `SEAWEEDFS_ACCESS_KEY_ID`, `SEAWEEDFS_SECRET_ACCESS_KEY`) +`REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SESSION_SECRET`, +`SMTP_USERNAME`, `SMTP_PASSWORD`, `SEAWEEDFS_ACCESS_KEY_ID`, +`SEAWEEDFS_SECRET_ACCESS_KEY`) with freshly randomized ones via `openssl`. Non-secret values are left untouched. The output file is written with `600` permissions. @@ -173,19 +176,26 @@ The active website does not use the old Next.js/Prisma environment model. Its core runtime variables are: ```bash -API_BASE_URL=http://localhost:8080 # Base URL for the .NET API +API_BASE_URL=http://api.core:8080 # Base URL for the .NET API (container DNS name in Docker) SESSION_SECRET= # Cookie session signing secret NODE_ENV=development # Standard Node runtime mode +PORT=3000 # Port the built server listens on ``` +The `frontend` Docker service hardcodes `API_BASE_URL` to `http://api.core:8080` +(the backend's in-network service name) and `PORT` to `3000`; only +`SESSION_SECRET` is sourced from the env file, and `NODE_ENV` is set per +compose file (`development` in dev, `production` in prod). + ### Frontend variable details #### `API_BASE_URL` - **Required**: Yes for local development - **Default in code**: `http://localhost:8080` -- **Used by**: `web/frontend/app/lib/auth.server.ts` -- **Purpose**: Routes website auth actions to the .NET API +- **Used by**: `web/frontend/app/features/auth/auth.server.ts`, + `web/frontend/app/features/breweries/breweries.server.ts` +- **Purpose**: Routes website auth and API calls to the .NET API #### `SESSION_SECRET` @@ -200,6 +210,12 @@ NODE_ENV=development # Standard Node runtime mode - **Typical values**: `development`, `production`, `test` - **Purpose**: Controls secure cookie behavior and runtime mode +#### `PORT` + +- **Required**: No +- **Default**: `3000` +- **Purpose**: Port the built server (`build/server/index.js`) binds to + ### SMTP configuration (backend) Read by `Infrastructure.Email/SmtpEmailProvider.cs` for sending confirmation and @@ -312,9 +328,10 @@ reference only and is not run as part of the active stack. | `SEAWEEDFS_ACCESS_KEY_ID` | ✓ | | ✓ | Yes | S3 access key | | `SEAWEEDFS_SECRET_ACCESS_KEY` | ✓ | | ✓ | Yes | S3 secret key | | `SEAWEEDFS_BUCKET` | ✓ | | ✓ | Yes | S3 bucket name | -| `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL | -| `SESSION_SECRET` | | ✓ | | Yes | Website session signing | -| `NODE_ENV` | | ✓ | | No | Runtime mode | +| `API_BASE_URL` | | ✓ | ✓ | Yes | Website-to-API base URL | +| `SESSION_SECRET` | | ✓ | ✓ | Yes | Website session signing | +| `NODE_ENV` | | ✓ | ✓ | No | Runtime mode | +| `PORT` | | ✓ | ✓ | No | Frontend server port | | `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag | | `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment | | `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address | @@ -366,6 +383,9 @@ REFRESH_TOKEN_SECRET= CONFIRMATION_TOKEN_SECRET= WEBSITE_BASE_URL=http://localhost:3000 +# Frontend session signing +SESSION_SECRET= + # SMTP (Mailpit in dev) SMTP_HOST=mailpit SMTP_PORT=1025 diff --git a/docs/website/testing.md b/docs/website/testing.md index e05e7eff..06164cc8 100644 --- a/docs/website/testing.md +++ b/docs/website/testing.md @@ -33,7 +33,8 @@ This command: 1. Starts a fresh SQL Server instance 2. Runs database migrations 3. Seeds test data -4. Executes all test suites in parallel +4. Executes all test suites in parallel, including the frontend's Storybook + Vitest and Playwright suites (`frontend.tests`, which needs no database) 5. Exports results to `./test-results/` 6. Exits when tests complete @@ -49,6 +50,8 @@ cat test-results/Features.Users.Tests.trx cat test-results/Features.Breweries.Tests.trx cat test-results/Features.Emails.Tests.trx cat test-results/Features.PhotoUpload.Tests.trx +cat test-results/frontend/vitest-storybook.xml +cat test-results/frontend/playwright-storybook.xml ``` ### Clean up @@ -327,8 +330,10 @@ Scenario: User login with valid credentials ## Continuous integration -Tests run automatically in CI/CD pipelines using the test Docker Compose -configuration: +The `.github/workflows/tests.yml` GitHub Actions workflow runs on every push +and pull request to `main`. It runs the same test Docker Compose +configuration used locally, so backend and frontend tests execute in the same +containerized environment in CI as they do on a developer machine: ```bash # CI/CD command @@ -337,13 +342,22 @@ docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abo docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v ``` +Because `--abort-on-container-exit` stops the stack as soon as any one-shot +test container exits (success or failure), the workflow separately inspects +the exit code of each test container (`test-env-api-specs`, +`test-env-unit-tests`, `test-env-frontend-tests`) after `up` returns and fails +the job if any of them is non-zero. `./test-results/` is uploaded as a build +artifact regardless of outcome. + Exit codes: - `0` - All tests passed - Non-zero - Test failures occurred -Frontend UI checks should also be included in CI for the active website -workspace: +The frontend's Storybook Vitest and Playwright suites run inside the +`frontend.tests` container (see [Docker Guide](docker.md)) as part of that +same compose run, so no separate frontend CI step is needed. To run them +locally without Docker: ```bash cd web/frontend diff --git a/web/.env.example b/web/.env.example index 8bcfbc07..bbad7355 100644 --- a/web/.env.example +++ b/web/.env.example @@ -39,6 +39,14 @@ ACCESS_TOKEN_SECRET=your-secure-jwt-secret-key REFRESH_TOKEN_SECRET=your-secure-jwt-refresh-secret-key CONFIRMATION_TOKEN_SECRET=your-secure-jwt-confirmation-secret-key +# ====================== +# Frontend (Website) Configuration +# ====================== + +# Cookie session signing secret for the React Router website container +# IMPORTANT: Generate a secure secret (minimum 32 characters) +# Command: openssl rand -base64 32 +SESSION_SECRET=your-secure-session-secret-key # ====================== # SMTP Configuration diff --git a/web/docker-compose.dev.yaml b/web/docker-compose.dev.yaml index c3fdf845..afdfa5d2 100644 --- a/web/docker-compose.dev.yaml +++ b/web/docker-compose.dev.yaml @@ -119,6 +119,27 @@ services: volumes: - nuget-cache-dev:/root/.nuget/packages + frontend: + env_file: ".env.dev" + image: frontend + container_name: dev-env-frontend + depends_on: + api.core: + condition: service_started + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + NODE_ENV: "development" + PORT: "3000" + API_BASE_URL: "http://api.core:8080" + SESSION_SECRET: "${SESSION_SECRET}" + restart: unless-stopped + networks: + - devnet + seaweedfs: env_file: ".env.dev" image: chrislusf/seaweedfs diff --git a/web/docker-compose.prod.yaml b/web/docker-compose.prod.yaml index 62c57608..d2248d07 100644 --- a/web/docker-compose.prod.yaml +++ b/web/docker-compose.prod.yaml @@ -59,9 +59,8 @@ services: args: BUILD_CONFIGURATION: Release APP_UID: 1000 - ports: - - "8080:8080" - - "8081:8081" + # No published ports: only the frontend service is reachable from outside + # the Docker network in production. api.core stays internal to prodnet. environment: ASPNETCORE_ENVIRONMENT: "Production" ASPNETCORE_URLS: "http://0.0.0.0:8080" @@ -80,6 +79,27 @@ services: volumes: - nuget-cache-prod:/root/.nuget/packages + frontend: + env_file: ".env.prod" + image: frontend + container_name: prod-env-frontend + depends_on: + api.core: + condition: service_started + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + NODE_ENV: "production" + PORT: "3000" + API_BASE_URL: "http://api.core:8080" + SESSION_SECRET: "${SESSION_SECRET}" + restart: unless-stopped + networks: + - prodnet + volumes: sqlserverdata-prod: driver: local diff --git a/web/docker-compose.test.yaml b/web/docker-compose.test.yaml index 516bdcf5..45405a94 100644 --- a/web/docker-compose.test.yaml +++ b/web/docker-compose.test.yaml @@ -139,6 +139,18 @@ services: networks: - testnet + frontend.tests: + image: frontend.tests + container_name: test-env-frontend-tests + build: + context: ./frontend + dockerfile: Dockerfile.tests + volumes: + - ./test-results:/app/test-results + restart: "no" + networks: + - testnet + volumes: sqlserverdata-test: driver: local diff --git a/web/frontend/.dockerignore b/web/frontend/.dockerignore new file mode 100644 index 00000000..c53c2cad --- /dev/null +++ b/web/frontend/.dockerignore @@ -0,0 +1,15 @@ +**/.dockerignore +**/.env +**/.env.* +**/.git +**/.gitignore +**/.vscode +**/.idea +**/node_modules +**/build +**/.react-router +**/storybook-static +**/coverage +**/test-results +**/docker-compose* +**/Dockerfile* diff --git a/web/frontend/Dockerfile b/web/frontend/Dockerfile new file mode 100644 index 00000000..59c2abff --- /dev/null +++ b/web/frontend/Dockerfile @@ -0,0 +1,29 @@ +# Stage 1: install every dependency (including dev deps needed to build) +FROM node:22-alpine AS deps +WORKDIR /src +COPY package.json package-lock.json ./ +RUN npm ci + +# Stage 2: build the client and server bundles (react-router build) +FROM deps AS build +WORKDIR /src +COPY . . +RUN npm run build + +# Stage 3: production-only dependencies for the runtime image +FROM node:22-alpine AS prod-deps +WORKDIR /src +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +# Stage 4: runtime +FROM node:22-alpine AS final +ENV NODE_ENV=production +WORKDIR /app +COPY package.json ./ +COPY --from=prod-deps /src/node_modules ./node_modules +COPY --from=build /src/build ./build +COPY --from=build /src/server.js ./server.js +EXPOSE 3000 +USER node +CMD ["node", "./server.js"] diff --git a/web/frontend/Dockerfile.tests b/web/frontend/Dockerfile.tests new file mode 100644 index 00000000..60565e52 --- /dev/null +++ b/web/frontend/Dockerfile.tests @@ -0,0 +1,10 @@ +# Ships Playwright's own browsers preinstalled, matching the @playwright/test +# version pinned in package.json, so the Storybook Vitest and Playwright +# suites (both browser-driven) don't need a separate browser-install step. +FROM mcr.microsoft.com/playwright:v1.62.1-jammy AS test +WORKDIR /src +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN mkdir -p /app/test-results/frontend +ENTRYPOINT ["sh", "-c", "set -e; npx vitest run --project storybook --reporter=junit --outputFile=/app/test-results/frontend/vitest-storybook.xml; PLAYWRIGHT_JUNIT_OUTPUT_NAME=/app/test-results/frontend/playwright-storybook.xml npx playwright test -c playwright.storybook.config.ts --reporter=junit"] diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json index 76c0d1b7..187ef399 100644 --- a/web/frontend/package-lock.json +++ b/web/frontend/package-lock.json @@ -13,6 +13,7 @@ "@react-router/dev": "^8.3.0", "@react-router/express": "^8.3.0", "@react-router/node": "^8.3.0", + "express": "^5.2.1", "iconoir-react": "^7.11.1", "isbot": "^5.2.1", "leaflet": "^1.9.4", @@ -3296,6 +3297,72 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", @@ -4618,7 +4685,6 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", - "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -5004,7 +5070,6 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", @@ -5029,7 +5094,6 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -5103,7 +5167,6 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -5326,7 +5389,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -5340,7 +5402,6 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -5356,7 +5417,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -5372,7 +5432,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.6.0" } @@ -5601,7 +5660,6 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -5664,8 +5722,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.412", @@ -5688,7 +5745,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -5963,8 +6019,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -6314,7 +6369,6 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -6346,7 +6400,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6447,7 +6500,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -6523,7 +6575,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -6547,7 +6598,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -6932,7 +6982,6 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", - "peer": true, "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", @@ -6966,7 +7015,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -7029,8 +7077,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -7052,7 +7099,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.10" } @@ -7349,8 +7395,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", @@ -8106,7 +8151,6 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" }, @@ -8120,7 +8164,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -8133,7 +8176,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -8143,7 +8185,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -8244,7 +8285,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "license": "MIT", - "peer": true, "dependencies": { "content-type": "^2.1.0" }, @@ -8261,7 +8301,6 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -8434,7 +8473,6 @@ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", - "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -8447,7 +8485,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", - "peer": true, "dependencies": { "wrappy": "1" } @@ -8639,7 +8676,6 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -8703,7 +8739,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -8918,7 +8953,6 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", - "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -8942,7 +8976,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" @@ -8959,7 +8992,6 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" }, @@ -8973,7 +9005,6 @@ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -9342,7 +9373,6 @@ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -9426,8 +9456,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/scheduler": { "version": "0.27.0", @@ -9452,7 +9481,6 @@ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", @@ -9479,7 +9507,6 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", - "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -9547,8 +9574,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -9698,7 +9724,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -10068,7 +10093,6 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.6" } @@ -10145,7 +10169,6 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", - "peer": true, "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", @@ -10164,7 +10187,6 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -10330,7 +10352,6 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -10419,7 +10440,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -10773,8 +10793,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/ws": { "version": "8.21.3", diff --git a/web/frontend/package.json b/web/frontend/package.json index 8c16e650..04c6bd35 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "react-router dev", "build": "react-router build", - "start": "NODE_ENV=production node ./build/server/index.js", + "start": "NODE_ENV=production node ./server.js", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier . --write", @@ -23,6 +23,7 @@ "@react-router/dev": "^8.3.0", "@react-router/express": "^8.3.0", "@react-router/node": "^8.3.0", + "express": "^5.2.1", "iconoir-react": "^7.11.1", "isbot": "^5.2.1", "leaflet": "^1.9.4", diff --git a/web/frontend/server.js b/web/frontend/server.js new file mode 100644 index 00000000..0fb51811 --- /dev/null +++ b/web/frontend/server.js @@ -0,0 +1,18 @@ +import { createRequestHandler } from '@react-router/express'; +import express from 'express'; + +const app = express(); + +app.use('/assets', express.static('build/client/assets', { immutable: true, maxAge: '1y' })); +app.use(express.static('build/client', { maxAge: '1h' })); + +const requestHandler = createRequestHandler({ + build: () => import('./build/server/index.js'), +}); +app.all('/', requestHandler); +app.all('/*splat', requestHandler); + +const port = process.env.PORT || 3000; +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); diff --git a/web/generate-env.sh b/web/generate-env.sh index 15d1ced2..2554c523 100755 --- a/web/generate-env.sh +++ b/web/generate-env.sh @@ -61,6 +61,7 @@ DB_CONNECTION_STRING_VAL="Server=${DB_SERVER_VAL};Database=${DB_NAME_VAL};User I ACCESS_TOKEN_SECRET_VAL=$(gen_jwt_secret) REFRESH_TOKEN_SECRET_VAL=$(gen_jwt_secret) CONFIRMATION_TOKEN_SECRET_VAL=$(gen_jwt_secret) +SESSION_SECRET_VAL=$(gen_jwt_secret) SMTP_USERNAME_VAL="smtp_$(gen_username)" SMTP_PASSWORD_VAL=$(gen_password) SEAWEEDFS_ACCESS_KEY_ID_VAL=$(gen_username) @@ -75,6 +76,7 @@ sed \ -e "s|^ACCESS_TOKEN_SECRET=.*|ACCESS_TOKEN_SECRET=${ACCESS_TOKEN_SECRET_VAL}|" \ -e "s|^REFRESH_TOKEN_SECRET=.*|REFRESH_TOKEN_SECRET=${REFRESH_TOKEN_SECRET_VAL}|" \ -e "s|^CONFIRMATION_TOKEN_SECRET=.*|CONFIRMATION_TOKEN_SECRET=${CONFIRMATION_TOKEN_SECRET_VAL}|" \ + -e "s|^SESSION_SECRET=.*|SESSION_SECRET=${SESSION_SECRET_VAL}|" \ -e "s|^SMTP_USERNAME=.*|SMTP_USERNAME=${SMTP_USERNAME_VAL}|" \ -e "s|^SMTP_PASSWORD=.*|SMTP_PASSWORD=${SMTP_PASSWORD_VAL}|" \ -e "s|^SEAWEEDFS_ACCESS_KEY_ID=.*|SEAWEEDFS_ACCESS_KEY_ID=${SEAWEEDFS_ACCESS_KEY_ID_VAL}|" \