diff --git a/README.md b/README.md index 42b4fb7..0a8cf06 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Schema migrations run automatically on first boot — no manual migration comman | `SELF_HOSTED` | First user gets all features unlocked | `false` | | `DISABLE_REGISTRATION` | Block new account creation (including OAuth auto-signup). First-user setup on an empty DB is still allowed. | `false` | | `DISABLE_HOMEPAGE` | Redirect `/` to `/app` instead of serving the marketing landing page. For internal-only self-hosted deployments. | `false` | -| `APP_URL` | Your public URL (used for Stripe callbacks) | _(none)_ | +| `APP_URL` | Your public URL (used for Stripe callbacks and invite-accept URLs in emailed invites) | _(none)_ | | `JWT_SECRET` | JWT signing key (auto-generated if not set) | _(auto)_ | | `SSL_CERT` | Path to SSL certificate | `server/certs/cert.pem` | | `SSL_KEY` | Path to SSL private key | `server/certs/key.pem` | diff --git a/server/routes/workspaces.js b/server/routes/workspaces.js index 9f0c7a6..86f092a 100644 --- a/server/routes/workspaces.js +++ b/server/routes/workspaces.js @@ -259,16 +259,18 @@ router.post('/:id/invites', async (req, res) => { return res.status(409).json({ error: 'An invite for this email is already pending' }); } - // Build accept URL. PUBLIC_URL env var (when set) pins the public-facing + // Build accept URL. APP_URL env var (when set) pins the public-facing // origin regardless of how the request arrived - recommended in prod so // invites triggered from non-browser sources (curl, future API automation) - // always carry the canonical origin. Falls back to request-derived for - // local dev and when PUBLIC_URL isn't set; with trust proxy on, req.protocol - // + req.get('host') reflect Cloudflare-forwarded X-Forwarded-Proto + Host. - // Path is /app#/accept-invite/ - the SPA lives at /app, so a bare - // /#/accept-invite/ would land on the marketing landing page in dev - // (and rely on the DISABLE_HOMEPAGE redirect in prod). /app is explicit. - const publicBase = process.env.PUBLIC_URL || `${req.protocol}://${req.get('host')}`; + // always carry the canonical origin. Same env var the rest of the codebase + // uses for Stripe callbacks (see README env-var table). Falls back to + // request-derived for local dev and when APP_URL isn't set; with trust + // proxy on, req.protocol + req.get('host') reflect Cloudflare-forwarded + // X-Forwarded-Proto + Host. Path is /app#/accept-invite/ - the SPA + // lives at /app, so a bare /#/accept-invite/ would land on the + // marketing landing page in dev (and rely on the DISABLE_HOMEPAGE + // redirect in prod). /app is explicit. + const publicBase = process.env.APP_URL || `${req.protocol}://${req.get('host')}`; const acceptUrl = `${publicBase}/app#/accept-invite/${inviteId}`; const org = db.prepare('SELECT name FROM organizations WHERE id = ?').get(ws.organization_id); const { subject, text } = buildInviteEmail({