diff --git a/docs/changelog/index.mdx b/docs/changelog/index.mdx index 0d69dfba3..1fe94cf48 100644 --- a/docs/changelog/index.mdx +++ b/docs/changelog/index.mdx @@ -4,12 +4,19 @@ description: "List of all notable changes and updates to Reactive Resume" rss: true --- - + ## Security -- Pinned some packages to specific versions to avoid potential security vulnerabilities through transitive dependencies. (through running `pnpm audit`) +- Hardened resume export and printer access controls to prevent unauthorized data access. +- PDF export is now owner-only; the public share-page download action has been removed. +- Strengthened protections for password verification, OAuth client registration, and redirect URI validation, including tighter rate limits. +- Blocked network-loading custom CSS patterns and improved CSS sanitization/scoping safeguards. +- Reduced SSRF risk by tightening URL validation for AI provider base URLs and user picture URLs. +- Disabled public auth API reference/schema exposure in production-facing auth routes. ## Maintenance -- Updated dependencies and lockfile. +- Updated dependencies and lockfile (including security-focused packages and typings). +- Added targeted tests for auth, printer/export, URL validation, and CSS sanitization hardening. +- Improved Docker Compose security defaults (safer Browserless token/image/network configuration). - Synced translation catalogs from Crowdin. diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 53db7147c..51e46fa04 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -67,28 +67,60 @@ Before you begin, ensure you have the following installed: - ```bash git clone https://github.com/amruthpillai/reactive-resume.git cd Reactive-Resume ``` + ```bash + git clone https://github.com/amruthpillai/reactive-resume.git + cd Reactive-Resume + ``` - Create a `.env` file in the root directory with the following variables: ```bash # Application - APP_URL=http://localhost:3000 # Database DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres # - Authentication (generate a secure secret) AUTH_SECRET=your-secure-secret-key-here # Storage (S3-compatible) - S3_ACCESS_KEY_ID=seaweedfs S3_SECRET_ACCESS_KEY=seaweedfs S3_ENDPOINT=http://seaweedfs:8333 - S3_BUCKET=reactive-resume S3_FORCE_PATH_STYLE=true ``` + Create a `.env` file in the root directory with the following variables: + +```bash .env +# Application +APP_URL=http://localhost:3000 + +# Printer (Browserless) +BROWSERLESS_TOKEN=change-me +PRINTER_ENDPOINT=ws://browserless:3000?token=change-me + +# Database +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres + +# Authentication (generate a secure secret) +AUTH_SECRET=your-secure-secret-key-here + +# Storage (S3-compatible via SeaweedFS) +S3_ACCESS_KEY_ID=seaweedfs +S3_SECRET_ACCESS_KEY=seaweedfs +S3_ENDPOINT=http://seaweedfs:8333 +S3_BUCKET=reactive-resume +S3_FORCE_PATH_STYLE=true +``` + - For production deployments, always use strong, unique values for `AUTH_SECRET` and database credentials. + For production deployments, always use strong, unique values for `AUTH_SECRET`, `BROWSERLESS_TOKEN`, and database credentials. - ```bash docker compose up -d ``` This will start: - **PostgreSQL** — Database for storing user data and resumes - - **SeaweedFS** — S3-compatible storage for file uploads - **Printer** — PDF and screenshot generation service - (browserless/chromium) - **Reactive Resume** — The main application + ```bash + docker compose up -d + ``` + + This starts: + - **PostgreSQL** — Database for storing user data and resumes + - **SeaweedFS** — S3-compatible storage for file uploads + - **Browserless** — Headless Chromium service for PDF/screenshot generation + - **Reactive Resume** — The main application - Once all services are running, access your Reactive Resume instance at: ``` http://localhost:3000 ``` + Once all services are running, access your Reactive Resume instance at: + + ```text + http://localhost:3000 + ``` @@ -97,12 +129,12 @@ Before you begin, ensure you have the following installed: Here's what each service in the stack does: -| Service | Port | Description | -| ----------- | ---- | ----------------------------------------------------------- | -| `postgres` | 5432 | PostgreSQL database for storing all application data | -| `seaweedfs` | 8333 | S3-compatible object storage for file uploads | -| `printer` | 4000 | Headless Chromium service for PDF and screenshot generation | -| `app` | 3000 | The main Reactive Resume application | +| Service | Port | Description | +| ----------------- | ---------------------------------- | ----------------------------------------------------------- | +| `postgres` | 5432 | PostgreSQL database for storing all application data | +| `seaweedfs` | 8333 | S3-compatible object storage for file uploads | +| `browserless` | 3000 (internal) / 4000 (published) | Headless Chromium service for PDF and screenshot generation | +| `reactive_resume` | 3000 | The main Reactive Resume application | ### Health Checks @@ -122,51 +154,58 @@ Here's a complete list of environment variables you can configure: ### Required Variables -| Variable | Description | Example | -| ------------------ | ------------------------------ | --------------------------------------- | -| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` | -| `AUTH_SECRET` | Secret key for authentication | Generate with `openssl rand -base64 32` | -| `APP_URL` | Public URL of your Application | `https://rxresu.me` | -| `PRINTER_ENDPOINT` | URL of the printer service | `http://printer:3000` | +| Variable | Description | Example | +| ------------------ | ------------------------------ | ------------------------------------- | +| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` | +| `AUTH_SECRET` | Secret key for authentication | Generate with `openssl rand -hex 32` | +| `APP_URL` | Public URL of your Application | `https://rxresu.me` | +| `PRINTER_ENDPOINT` | URL of the printer service | `ws://browserless:3000?token=...` | ### Optional Variables -| Variable | Description | Default | -| ------------------------------- | ----------------------------------------------------------- | ---------------------- | -| `PRINTER_APP_URL` | Public URL for the printer to access the Application | — | -| `GOOGLE_CLIENT_ID` | Google OAuth Client ID | — | -| `GOOGLE_CLIENT_SECRET` | Google OAuth Client Secret | — | -| `GITHUB_CLIENT_ID` | GitHub OAuth Client ID | — | -| `GITHUB_CLIENT_SECRET` | GitHub OAuth Client Secret | — | -| `LINKEDIN_CLIENT_ID` | LinkedIn OAuth Client ID | — | -| `LINKEDIN_CLIENT_SECRET` | LinkedIn OAuth Client Secret | — | -| `OAUTH_PROVIDER_NAME` | Custom OAuth Provider Name | — | -| `OAUTH_CLIENT_ID` | Custom OAuth Client ID | — | -| `OAUTH_CLIENT_SECRET` | Custom OAuth Client Secret | — | -| `OAUTH_DISCOVERY_URL` | OIDC Discovery URL (use this OR manual URLs below) | — | -| `OAUTH_AUTHORIZATION_URL` | OAuth Authorization URL (manual config) | — | -| `OAUTH_TOKEN_URL` | OAuth Token URL (manual config) | — | -| `OAUTH_USER_INFO_URL` | OAuth User Info URL (manual config) | — | -| `OAUTH_SCOPES` | OAuth Scopes (space-separated) | `openid profile email` | -| `SMTP_HOST` | SMTP Server Host (for email features) | — | -| `SMTP_PORT` | SMTP Server Port | `587` | -| `SMTP_USER` | SMTP Username | — | -| `SMTP_PASS` | SMTP Password | — | -| `SMTP_FROM` | Default FROM address for emails | — | -| `SMTP_SECURE` | Use secure SMTP connection (`true` or `false`) | `false` | -| `S3_ACCESS_KEY_ID` | S3 Access Key | — | -| `S3_SECRET_ACCESS_KEY` | S3 Secret Key | — | -| `S3_REGION` | S3 Region | `us-east-1` | -| `S3_ENDPOINT` | S3-compatible Endpoint URL | — | -| `S3_BUCKET` | S3 Bucket Name | — | -| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 (set `true` for MinIO/SeaweedFS) | `false` | -| `FLAG_DEBUG_PRINTER` | Used for debugging the printer route | `false` | -| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` | -| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` | -| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` | +| Variable | Description | Default | +| ------------------------------------- | ----------------------------------------------------------- | ---------------------- | +| `PRINTER_APP_URL` | Public URL for the printer to access the Application | — | +| `GOOGLE_CLIENT_ID` | Google OAuth Client ID | — | +| `GOOGLE_CLIENT_SECRET` | Google OAuth Client Secret | — | +| `GITHUB_CLIENT_ID` | GitHub OAuth Client ID | — | +| `GITHUB_CLIENT_SECRET` | GitHub OAuth Client Secret | — | +| `LINKEDIN_CLIENT_ID` | LinkedIn OAuth Client ID | — | +| `LINKEDIN_CLIENT_SECRET` | LinkedIn OAuth Client Secret | — | +| `OAUTH_PROVIDER_NAME` | Custom OAuth Provider Name | — | +| `OAUTH_CLIENT_ID` | Custom OAuth Client ID | — | +| `OAUTH_CLIENT_SECRET` | Custom OAuth Client Secret | — | +| `OAUTH_DISCOVERY_URL` | OIDC Discovery URL (use this OR manual URLs below) | — | +| `OAUTH_AUTHORIZATION_URL` | OAuth Authorization URL (manual config) | — | +| `OAUTH_TOKEN_URL` | OAuth Token URL (manual config) | — | +| `OAUTH_USER_INFO_URL` | OAuth User Info URL (manual config) | — | +| `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` | Trusted HTTPS hosts/origins for dynamic OAuth redirects | — | +| `OAUTH_SCOPES` | OAuth Scopes (space-separated) | `openid profile email` | +| `BETTER_AUTH_API_KEY` | Better Auth dashboard API key | — | +| `BETTER_AUTH_URL` | Better Auth base URL override (advanced) | `APP_URL` | +| `BETTER_AUTH_SECRET` | Better Auth secret override (advanced) | `AUTH_SECRET` | +| `AI_ALLOWED_BASE_URLS` | Allowlist for custom AI provider base URLs | — | +| `SMTP_HOST` | SMTP Server Host (for email features) | — | +| `SMTP_PORT` | SMTP Server Port | `587` | +| `SMTP_USER` | SMTP Username | — | +| `SMTP_PASS` | SMTP Password | — | +| `SMTP_FROM` | Default FROM address for emails | — | +| `SMTP_SECURE` | Use secure SMTP connection (`true` or `false`) | `false` | +| `S3_ACCESS_KEY_ID` | S3 Access Key | — | +| `S3_SECRET_ACCESS_KEY` | S3 Secret Key | — | +| `S3_REGION` | S3 Region | `us-east-1` | +| `S3_ENDPOINT` | S3-compatible Endpoint URL | — | +| `S3_BUCKET` | S3 Bucket Name | — | +| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 (set `true` for MinIO/SeaweedFS) | `false` | +| `FLAG_DEBUG_PRINTER` | Used for debugging the printer route | `false` | +| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` | +| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` | +| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` | > **Note:** Some variables are only required for using related features (OAuth, SMTP, S3, etc.) and can be left unset if unused. +> **Health check behavior:** `/api/health` reports status for database, printer, and storage. A failure in any dependency returns HTTP `503`. + **Hybrid Setup Note**: The `PRINTER_APP_URL` variable is required when running Reactive Resume outside of Docker while the printer service is running inside Docker. In this scenario, the printer needs to reach your local app to render diff --git a/docs/self-hosting/docker.mdx b/docs/self-hosting/docker.mdx index 1a8504e6f..3a084de68 100644 --- a/docs/self-hosting/docker.mdx +++ b/docs/self-hosting/docker.mdx @@ -54,9 +54,9 @@ APP_URL="http://localhost:3000" PRINTER_APP_URL="http://host.docker.internal:3000" # --- Printer --- -# If using browserless with token authentication, include the token as a query parameter: -# PRINTER_ENDPOINT="ws://printer:3000?token=your-secret-token" -PRINTER_ENDPOINT="ws://printer:3000" +# Keep this token in sync with the Browserless TOKEN value. +BROWSERLESS_TOKEN="change-me" +PRINTER_ENDPOINT="ws://printer:3000?token=change-me" # --- Database (PostgreSQL) --- DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres" @@ -64,6 +64,8 @@ DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres" # --- Authentication --- # Generated using `openssl rand -hex 32` AUTH_SECRET="" +# Better Auth dashboard API key (optional) +BETTER_AUTH_API_KEY="" # Social Auth (Google, optional) GOOGLE_CLIENT_ID="" @@ -87,20 +89,30 @@ OAUTH_DISCOVERY_URL="" OAUTH_AUTHORIZATION_URL="" OAUTH_TOKEN_URL="" OAUTH_USER_INFO_URL="" +OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS="" # Custom scopes (space-separated, defaults to "openid profile email") OAUTH_SCOPES="" +# Optional Better Auth runtime overrides for advanced deployments: +# BETTER_AUTH_URL="https://auth.example.com" +# BETTER_AUTH_SECRET="" + +# --- AI (optional) --- +# Comma-separated hostnames/origins for custom AI base URLs +# Example: api.openai.com,https://gateway.ai.vercel.com +AI_ALLOWED_BASE_URLS="" + # --- Email (optional) --- # If all keys are disabled, the app logs the email to be sent to the console instead. SMTP_HOST="" -SMTP_PORT="465" +SMTP_PORT="587" SMTP_USER="" SMTP_PASS="" SMTP_FROM="Reactive Resume " SMTP_SECURE="false" # --- Storage (optional) --- -# If all keys are disabled, the app uses local filesystem (/data) to store uploads instead. +# If all keys are disabled, the app uses local filesystem (usually /app/data) to store uploads instead. # Make sure to mount this directory to a volume or the host filesystem to ensure data integrity. S3_ACCESS_KEY_ID="" S3_SECRET_ACCESS_KEY="" @@ -115,6 +127,7 @@ S3_FORCE_PATH_STYLE="false" FLAG_DEBUG_PRINTER="false" FLAG_DISABLE_SIGNUPS="false" FLAG_DISABLE_EMAIL_AUTH="false" +FLAG_DISABLE_IMAGE_PROCESSING="false" ``` @@ -134,9 +147,6 @@ FLAG_DISABLE_EMAIL_AUTH="false" [byte[]]$bytes = New-Object byte[] 32; (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($bytes); $bytes | ForEach-Object { "{0:x2}" -f $_ } | Out-String -Stream | ForEach-Object { $_.Trim() } | Write-Host -NoNewline ``` - ```cmd Windows (alternative) - certutil -generateSRS 32 | findstr /r /v "^$" | findstr /v ":" | findstr /v " " | findstr /v "-" | findstr /v "certutil" - ``` @@ -172,10 +182,9 @@ services: - HEALTH=true - CONCURRENT=20 - QUEUED=10 - # Optional: Set a token for authentication - # - TOKEN=your-secret-token + - TOKEN=${BROWSERLESS_TOKEN} healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"] + test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null'] interval: 10s timeout: 5s retries: 10 @@ -282,11 +291,15 @@ docker compose logs -f reactive-resume SMTP (SMTP_*)
  • - Social auth (GOOGLE_*, GITHUB_*) + Social auth (GOOGLE_*, GITHUB_*, LINKEDIN_*,{" "} + OAUTH_*)
  • S3 storage (S3_*)
  • +
  • + AI URL allowlist (AI_ALLOWED_BASE_URLS) +
  • Feature flags (FLAG_*)
  • @@ -302,7 +315,9 @@ docker compose logs -f reactive-resume - - **`PRINTER_ENDPOINT`**: Base URL where Reactive Resume reaches the printer service. In Compose: `http://printer:3000`. If using browserless with token authentication, include the token as a query parameter: `ws://printer:3000?token=your-secret-token`. + - **`PRINTER_ENDPOINT`**: Endpoint where Reactive Resume connects to the printer browser. + - **Recommended (Browserless)**: `ws://printer:3000?token=...` and keep the token value in sync with Browserless `TOKEN`. + - **Also supported**: `http://chrome:9222` for Chrome DevTools Protocol endpoints. **Alternative to browserless**: You can use a lightweight headless Chrome Docker image like `chromedp/headless-shell`: @@ -322,7 +337,9 @@ docker compose logs -f reactive-resume - **`DATABASE_URL`**: Postgres connection string in the format `postgresql://USER:PASSWORD@HOST:PORT/DATABASE`. - In - Docker Compose, set `HOST` to the Postgres service name (e.g. `postgres`), not `localhost`. + Docker Compose, set `HOST` to the Postgres service name (e.g. `postgres`), not `localhost`. - If your password + contains special characters (`@`, `#`, `:`), URL-encode it. - For managed Postgres, add provider-specific params (for + example `?sslmode=require`) when needed. @@ -344,9 +361,16 @@ openssl rand -hex 32 **`LINKEDIN_CLIENT_ID`** / **`LINKEDIN_CLIENT_SECRET`** (optional): Enables LinkedIn sign-in. + **`BETTER_AUTH_API_KEY`** (optional): Enables Better Auth dashboard integrations. + + **`BETTER_AUTH_URL`** (optional, advanced): Overrides auth base URL if it must differ from `APP_URL` (for split-host deployments). + + **`BETTER_AUTH_SECRET`** (optional, advanced): Overrides `AUTH_SECRET` for Better Auth internals. + **Custom OAuth provider** (optional): - **`OAUTH_PROVIDER_NAME`**: Display name in the UI - **`OAUTH_CLIENT_ID`** / **`OAUTH_CLIENT_SECRET`**: Required for any custom OAuth provider + - **`OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS`**: Comma-separated allowlist for extra dynamic OAuth redirect hosts/origins (HTTPS only, non-private hosts). - **`OAUTH_SCOPES`**: Space-separated scopes (defaults to `openid profile email`) Configure endpoints using **one** of these methods: @@ -358,8 +382,9 @@ openssl rand -hex 32 If SMTP is not configured, the app logs emails to the server console instead of sending them. + - Email delivery is enabled only when **all** of `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, and `SMTP_FROM` are set. - **`SMTP_HOST`**: SMTP host (if empty, email sending is disabled). - - **`SMTP_PORT`**: Usually `465` (implicit TLS) or `587` (STARTTLS). + - **`SMTP_PORT`**: Defaults to `587` in the app. - **`SMTP_USER`** / **`SMTP_PASS`**: SMTP credentials. - **`SMTP_FROM`**: Default from address (for example, `Reactive Resume `). - **`SMTP_SECURE`**: `"true"` or `"false"` (string). Match your provider settings. @@ -367,15 +392,17 @@ openssl rand -hex 32 - - **Default (local)**: If all `S3_*` values are empty, uploads are stored under `/app/data`. Mount it to persistent - storage (for example `./data:/app/data`) or uploads can be lost on container recreation. - **S3/S3-compatible**: - Configure these to store uploads in an S3-compatible service (SeaweedFS, MinIO, AWS S3, etc.): - - **`S3_ACCESS_KEY_ID`** - **`S3_SECRET_ACCESS_KEY`** - **`S3_REGION`** - **`S3_ENDPOINT`** (for S3-compatible - providers; may be blank for AWS depending on your setup) - **`S3_BUCKET`** - **`S3_FORCE_PATH_STYLE`**: Controls how - the bucket is addressed in URLs. Defaults to `"false"`. - Set to `"true"` for **path-style** URLs - (`https://s3-server.com/bucket`). Common with **MinIO**, **SeaweedFS**, and other self-hosted S3-compatible services. - - Set to `"false"` for **virtual-hosted-style** URLs (`https://bucket.s3-server.com`). Common with **AWS S3**, - **Cloudflare R2**, and most cloud providers. + - **Default (local)**: If all `S3_*` values are empty, uploads are stored under `/data` (usually `/app/data` in the official image). + - Mount local uploads to persistent storage (for example `./data:/app/data`) or uploads can be lost on container recreation. + - **S3/S3-compatible**: Configure `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`, `S3_ENDPOINT`, and `S3_BUCKET`. + - **`S3_FORCE_PATH_STYLE`** controls bucket addressing (defaults to `"false"`): + - `"true"` for path-style URLs (`https://endpoint/bucket`) common with MinIO/SeaweedFS. + - `"false"` for virtual-hosted-style URLs (`https://bucket.endpoint`) common with AWS S3 / Cloudflare R2. + + + + - **`AI_ALLOWED_BASE_URLS`**: Comma-separated hosts or origins allowed as custom AI API base URLs. - Use this when + routing AI requests through your own gateway/proxy. - Example: `api.openai.com,https://gateway.ai.vercel.com` @@ -390,24 +417,32 @@ openssl rand -hex 32 To update your Reactive Resume installation to the latest available version, follow these steps: -1. **Pull the latest images** for all services defined in your Docker Compose file. +1. **Back up your database and uploads first** (highly recommended before every update). + +2. **Pull the latest images** for all services defined in your Docker Compose file. ```bash docker compose pull ``` -2. **Restart the containers** to run the new images. +3. **Restart the containers** to run the new images. ```bash docker compose up -d ``` -3. **(Optional) Remove old, unused Docker images** to free up disk space. +4. **Check migration/startup logs** after deploy. + + ```bash + docker compose logs -f reactive-resume + ``` + +5. **(Optional) Remove old, unused Docker images** to free up disk space. ```bash docker image prune -f ``` -This process ensures your app, database, and printer are all up-to-date while keeping your data and configuration intact. +This process updates app services and automatically runs DB migrations on startup. If migration fails, restore from backup and fix configuration before retrying. ## Backups (recommended) @@ -425,7 +460,7 @@ If you're using S3-compatible storage, consider enabling versioning on your buck ## Health Checks -Reactive Resume exposes a health check endpoint at `/api/health` that verifies the application and its dependencies are functioning correctly. If any critical service (such as the database connection) fails, the health check will return an unhealthy status. +Reactive Resume exposes a health check endpoint at `/api/health` that verifies the application and its dependencies. It checks **database**, **printer**, and **storage**; if any one is unhealthy, the endpoint returns HTTP `503`. ### How it works @@ -448,7 +483,7 @@ Most reverse proxies (such as **Traefik**, **Caddy**, or **nginx** with upstream - **Healthy containers** receive traffic as normal - **Unhealthy containers** are automatically removed from the load balancer pool -This is particularly useful in high-availability setups where you have multiple instances of Reactive Resume. If one instance becomes unhealthy (for example, it loses its database connection), the reverse proxy will stop routing traffic to it until it recovers. +This is particularly useful in high-availability setups where you have multiple instances of Reactive Resume. If one instance becomes unhealthy (for example, it loses database, printer, or storage connectivity), the reverse proxy will stop routing traffic to it until it recovers. If you're using **Traefik**, it automatically respects Docker health checks when using the Docker provider. Unhealthy @@ -467,40 +502,51 @@ curl -f http://localhost:3000/api/health docker compose ps ``` -A healthy response returns HTTP 200. Any other response (or a connection failure) indicates a problem that should be investigated in the container logs. +A healthy response returns HTTP 200. Any other response (or a connection failure) indicates a problem that should be investigated in the JSON response body and container logs. ## Troubleshooting - **Common cause**: database migrations failed (often a bad `DATABASE_URL`). - - **What to do**: - When the app container exits right away, you'll want to check the logs for more information about the error. Run the following command to view real-time logs from the Reactive Resume container: + - **What to do**: + Check logs for migration errors and database connectivity details: ```bash docker compose logs -f reactive-resume ``` - - **Common cause**: `APP_URL` doesn't match the URL you're actually using (especially behind a reverse proxy). - - **Fix**: set `APP_URL` to the public URL (preferably HTTPS) and restart the container. + - **Common cause**: `APP_URL` doesn't match the URL you're actually using (especially behind a reverse proxy), or + you're serving HTTPS while `APP_URL` is `http://...`. - **Fix**: set `APP_URL` to your canonical public HTTPS URL and + restart the container. - - **Common cause**: Reactive Resume can't reach the printer or the printer can't reach your app. - **Checks**: - - `PRINTER_ENDPOINT` should usually be `http://printer:3000` in Compose. - If you use - `PRINTER_APP_URL="http://host.docker.internal:3000"`, ensure `extra_hosts: host-gateway` is present for the printer - service. + - **Common cause**: Reactive Resume can't reach the printer, token mismatch, or the printer can't reach your app. - + **Checks**: - `PRINTER_ENDPOINT` should usually be `ws://printer:3000?token=...` in Compose. - Browserless `TOKEN` and + the token in `PRINTER_ENDPOINT` must match. - If you use `PRINTER_APP_URL="http://host.docker.internal:3000"` on + Linux, set `extra_hosts: ["host.docker.internal:host-gateway"]` for the printer service. + + + + - **Common cause**: printer or storage health failed (not only database). - **Fix**: inspect the endpoint response + payload and check `printer` / `storage` fields: ```bash curl -s http://localhost:3000/api/health ``` - - **Cause**: you didn't mount persistent storage for `/app/data` (when not using S3). - **Fix**: add a volume mount - like `./data:/app/data` and redeploy. + - **Cause**: local upload storage wasn't mounted to a persistent volume. - **Fix**: add a volume mount like + `./data:/app/data` and redeploy. - - **Expected behavior**: if SMTP vars are empty, the app logs emails to the console instead. - **Fix**: configure SMTP - and verify your provider's TLS/port settings. + - **Expected behavior**: if SMTP isn't fully configured, the app logs emails to the console. - **Fix**: set + `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, and `SMTP_FROM`, then verify `SMTP_PORT` and `SMTP_SECURE`. + + + + - **Common cause**: redirect host is not trusted for dynamic client registration. - **Fix**: add trusted HTTPS + hosts/origins to `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS`. diff --git a/docs/self-hosting/examples.mdx b/docs/self-hosting/examples.mdx index 1ef9dd95a..76d6fdc52 100644 --- a/docs/self-hosting/examples.mdx +++ b/docs/self-hosting/examples.mdx @@ -85,12 +85,11 @@ services: - QUEUED=10 - HEALTH=true - CONCURRENT=5 - # Optional: Set a token for authentication - # - TOKEN=your-secret-token + - TOKEN=${BROWSERLESS_TOKEN} networks: - reactive_resume_network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"] + test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null'] interval: 30s timeout: 10s retries: 3 @@ -102,7 +101,7 @@ services: - APP_URL=https://resume.${DOMAIN} - PRINTER_APP_URL=http://reactive_resume:3000 - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres - - PRINTER_ENDPOINT=http://printer:3000 + - PRINTER_ENDPOINT=ws://printer:3000?token=${BROWSERLESS_TOKEN} - AUTH_SECRET=${AUTH_SECRET} # Add other optional env vars as needed (SMTP, S3, OAuth, etc.) volumes: @@ -143,6 +142,7 @@ DOMAIN="example.com" ACME_EMAIL="admin@example.com" POSTGRES_PASSWORD="your-secure-postgres-password" AUTH_SECRET="your-auth-secret-from-openssl-rand-hex-32" +BROWSERLESS_TOKEN="your-browserless-token" # Optional: Traefik dashboard auth (generate with: htpasswd -nb admin password) TRAEFIK_DASHBOARD_AUTH="admin:$$apr1$$..." ``` @@ -191,10 +191,14 @@ services: - QUEUED=10 - HEALTH=true - CONCURRENT=5 - # Optional: Set a token for authentication - # - TOKEN=your-secret-token + - TOKEN=${BROWSERLESS_TOKEN} networks: - reactive_resume_network + healthcheck: + test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=${BROWSERLESS_TOKEN}" > /dev/null'] + interval: 30s + timeout: 10s + retries: 3 reactive_resume: image: amruthpillai/reactive-resume:latest @@ -203,7 +207,7 @@ services: - APP_URL=https://resume.${DOMAIN} - PRINTER_APP_URL=http://reactive_resume:3000 - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres - - PRINTER_ENDPOINT=http://printer:3000 + - PRINTER_ENDPOINT=ws://printer:3000?token=${BROWSERLESS_TOKEN} - AUTH_SECRET=${AUTH_SECRET} # Add other optional env vars as needed (SMTP, S3, OAuth, etc.) volumes: @@ -339,10 +343,9 @@ services: - QUEUED=10 - HEALTH=true - CONCURRENT=5 - # Optional: Set a token for authentication - # - TOKEN=your-secret-token + - TOKEN=$BROWSERLESS_TOKEN healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"] + test: ["CMD-SHELL", 'curl -fsS "http://localhost:3000/pressure?token=$BROWSERLESS_TOKEN" > /dev/null'] interval: 30s timeout: 10s retries: 3 @@ -469,10 +472,10 @@ docker stack deploy -c compose-swarm.yml reactive_resume docker stack services reactive_resume # View logs for the app -docker service logs -f reactive_resume_app +docker service logs -f reactive_resume_reactive_resume # Scale the app -docker service scale reactive_resume_app=3 +docker service scale reactive_resume_reactive_resume=3 # Remove the stack docker stack rm reactive_resume diff --git a/docs/self-hosting/migration.mdx b/docs/self-hosting/migration.mdx index b11345deb..e6b93d415 100644 --- a/docs/self-hosting/migration.mdx +++ b/docs/self-hosting/migration.mdx @@ -7,6 +7,11 @@ description: "A step-by-step guide to migrate your Reactive Resume instance from This guide walks you through migrating your Reactive Resume installation from **v4 to v5**. The migration process involves setting up a new v5 instance alongside your existing v4 instance, then transferring your users and resumes to the new system. + + This page is for **v4 → v5 data migration** only. For normal v5 upgrades, use the [Self-Hosting with + Docker](/self-hosting/docker) guide. v5 schema migrations run automatically on app startup. + + **Keep your v4 instance running** until you have successfully migrated all data to v5 and verified everything works correctly. This ensures you have a fallback in case anything goes wrong during the migration. @@ -86,7 +91,7 @@ To run the migration scripts, you need the following installed on your host mach Clone the Reactive Resume repository to access the migration scripts: ```bash git clone - https://github.com/amruthpillai/reactive-resume.git cd reactive-resume pnpm install ``` + https://github.com/amruthpillai/reactive-resume.git cd reactive-resume vp install ``` @@ -107,6 +112,8 @@ PRODUCTION_DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resu `PRODUCTION_DATABASE_URL` should point to your **old v4 database**. Mixing these up could cause data loss. +`PRODUCTION_DATABASE_URL` is used only by these migration scripts. It is not a runtime app variable. + ### Step 1: Migrate Users The user migration script transfers all user accounts, authentication data, and two-factor settings from v4 to v5. diff --git a/docs/self-hosting/sso.mdx b/docs/self-hosting/sso.mdx index 1e99d4a59..2fd7afe03 100644 --- a/docs/self-hosting/sso.mdx +++ b/docs/self-hosting/sso.mdx @@ -62,10 +62,13 @@ You must configure endpoints using **one** of these two methods: ### Optional Variables -| Variable | Description | Default | -| --------------------- | ---------------------------------------- | ---------------------- | -| `OAUTH_PROVIDER_NAME` | Display name shown on the sign-in button | `Custom OAuth` | -| `OAUTH_SCOPES` | Space-separated list of OAuth scopes | `openid profile email` | +| Variable | Description | Default | +| ------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------- | +| `OAUTH_PROVIDER_NAME` | Display name shown on the sign-in button | `Custom OAuth` | +| `OAUTH_SCOPES` | Space-separated list of OAuth scopes | `openid profile email` | +| `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` | Comma-separated allowlist for dynamic OAuth client redirect hosts/origins (HTTPS only) | _empty_ | +| `BETTER_AUTH_URL` | Optional auth base URL override for split-host setups | `APP_URL` | +| `BETTER_AUTH_SECRET` | Optional Better Auth secret override | `AUTH_SECRET` | ## Callback URL @@ -86,6 +89,18 @@ https://resume.example.com/api/auth/oauth2/callback/custom authentication to fail. + + Built-in providers (Google, GitHub, LinkedIn) use callback URLs in this format: `{APP_URL}/api/auth/callback/ + {provider}` (for example `.../google`, `.../github`, `.../linkedin`). + + +## URL and Proxy Requirements + +- Set `APP_URL` to the exact public URL users access (prefer HTTPS in production). +- If auth metadata/JWKS must be served from a different public host, set `BETTER_AUTH_URL`. +- Behind a reverse proxy, forward `Host` and `X-Forwarded-Proto` correctly, or cookie/session behavior may break. +- `trustedOrigins` are derived from `APP_URL`, so alternate domains are not automatically trusted. + ## Profile Mapping Reactive Resume automatically maps user profile data from the OAuth provider. The following fields are used: @@ -280,6 +295,11 @@ OAUTH_DISCOVERY_URL="https://auth.company.com/application/o/reactive-resume/.wel number differences - Path case sensitivity + + Common cause: `APP_URL` does not match the real HTTPS public origin (for example, app is behind TLS but `APP_URL` is + `http://...`). Fix: set `APP_URL` to the canonical HTTPS URL and restart the app. + + The custom OAuth option only appears if both `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are set, **and** either: - `OAUTH_DISCOVERY_URL` is set, **or** @@ -295,6 +315,11 @@ OAUTH_DISCOVERY_URL="https://auth.company.com/application/o/reactive-resume/.wel domain + + Dynamic OAuth client registration only allows HTTPS redirect URIs on trusted hosts. Add allowed hosts/origins to + `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` (comma-separated). + + The profile mapping depends on your provider returning standard claims: - `email` (required) @@ -319,6 +344,9 @@ OAUTH_DISCOVERY_URL="https://auth.company.com/application/o/reactive-resume/.wel Configure your OAuth provider to only allow the exact redirect URI. Avoid wildcards in redirect URI configurations. + + Keep `BETTER_AUTH_SECRET` and `BETTER_AUTH_API_KEY` private. Rotating these values may invalidate active sessions. + Only request the scopes you need. The default (`openid profile email`) is sufficient for Reactive Resume.