openvibers/openvibe.community Commits
Deploy: systemd unit and nginx vhost for the Node app; README rewritten
Changes
@@@ -1,18 +1,144 @@# OpenVibe.Community--Placeholder for the community hub of the OpenVibe network (https://openvibe.community).++**https://openvibe.community — the people of OpenVibe.**--The direction for this service is intentionally undecided. For now it is a single static--page (`public/index.html`) served directly by nginx — there is no application process.++The community hub of the OpenVibe network: community-run, open source, free speech within++the rules. Today it is the home of **pastes** (code, text and screenshots with a link);++spaces, threads and submissions follow.++++It is a small Node/Express app (CommonJS, no framework, no database of its own) that++server-renders every page — crawlers and no-JS readers get the whole thing — and adds a++little progressive JavaScript for pagination, copy buttons and the upload path.++++## How it fits the network++++| Concern | Where it lives | How Community reaches it |++| --- | --- | --- |++| Paste storage | **OpenVibe.Media** (`pastes` table, `/api/v1/:app/pastes`) | never directly |++| Paste API + account mapping | **OpenVibe.Live** (`/api/pastes/*`) | `OV_LIVE_INTERNAL_URL` (server-side) |++| Identity / SSO | **OpenVibe.Network** (OAuth2 + RS256 JWKS) | OAuth client `community` |++| Raw text, screenshots | OpenVibe.Media public host | 302 from `/p/:slug/raw`, `/p/:slug/screenshot` |++| Shared chrome, themes | `https://openvibe.network/shared/*.js` | loaded in every page |++++Why go through Live and not straight to Media: the visitor's JWT names the account by its++**Network** id, while the `user_id` Media stores for pastes is **Live's** own — different++numbers for the same person. Live resolves the visitor against its own accounts before it++writes, which only Live can do, so Community proxies `/api/pastes/*` to Live and forwards the++visitor's `Authorization: Bearer <network jwt>` (or `ov_token` cookie). Creation, listing,++anonymous-post limits, ownership and author names are therefore identical to Live's.++++## Endpoints++++Pages (server-rendered HTML):++++| Route | What |++| --- | --- |++| `GET /` | Home: hero, latest pastes, most viewed, "start a paste" CTA, what is coming |++| `GET /pastes` | Browse — `?q=` search, `?sort=new\|views`, `?lang=`, `?page=` |++| `GET /p/:slug` | Paste page — highlighted body (server-side, highlight.js), raw/download/copy/fork/share, screenshot, related |++| `GET /p/:slug/raw` | 302 → `https://openvibe.media/p/:slug/raw` |++| `GET /p/:slug/screenshot` | 302 → `https://openvibe.media/p/:slug/screenshot` |++| `GET /p/:slug/download` | The text as an attachment (`slug.ext`); screenshots bounce to Media |++| `GET /new`, `POST /new` | Create (signed-in or anonymous). `?fork=slug` prefills. The POST is the no-JS fallback; with JS the form talks to `/api/pastes` |++| `GET /my` | The signed-in user's pastes (public, unlisted and private); anonymous → sign-in |++++API and machine endpoints:++++| Route | What |++| --- | --- |++| `ANY /api/pastes/*` | Transparent proxy to Live `/api/pastes/*` — list, get, create, `screenshot` (multipart), `:slug/copy`, `:slug/like`, comments, delete… bodies stream through untouched |++| `GET /api/health`, `GET /api/ready` | Liveness / readiness |++| `GET /auth/login` | → Network `/oauth/authorize`. `?next=` (same-site path, this origin, or `https://openvibe.network/…`), `?silent=1` adds `prompt=none` |++| `GET /auth/callback` | Code exchange; sets cookies. `error=login_required` → `next` + `?sso=none` |++| `GET /auth/logout` | Clears session, sets `ov_sso_hint=guest`, honours `?next=` |++| `GET /auth/me` | Offline-verified profile from `ov_token` |++| `POST /auth/refresh` | Rotate via refresh token |++| `GET /robots.txt`, `GET /sitemap.xml`, `GET /feed.xml` | SEO + RSS of the latest pastes |++++Cookies are host-only for `openvibe.community`: `ov_token` (24 h access JWT, JS-readable so++the shared navbar can use it), `ov_refresh` (httpOnly, `/auth`), `ov_sso_hint`++(`account`/`guest`, 1 year, JS-readable — the navbar only tries a silent sign-in when it says++`account`).++++## SEO++++Every page carries a title, description, canonical, robots, Open Graph + Twitter card and++JSON-LD (`WebSite` on the home page, `Article`/`ImageObject` with author and `datePublished`++on paste pages, `BreadcrumbList` on both). Unlisted/private pastes and search result pages++are `noindex`. The sitemap lists home, `/pastes` and the latest public pastes (cached 1 h).++++## Configuration++++Copy `.env.example` to `.env` (production: `/etc/openvibe/community.env`, mode 0600).++++| Variable | Default | Meaning |++| --- | --- | --- |++| `PORT` / `HOST` | `4200` / `127.0.0.1` | Listen address (nginx in front) |++| `BASE_URL` | `https://openvibe.community` in production | Canonical origin |++| `TRUST_PROXY` | `2` | X-Forwarded-For hops (Cloudflare → nginx → Node) |++| `OV_NETWORK_URL` | `https://openvibe.network` | Issuer / authorize URL / JWKS |++| `OV_NETWORK_INTERNAL_URL` | `http://127.0.0.1:4000` | Token grants, JWKS (tried first) |++| `OV_OAUTH_CLIENT_ID` | `community` | Registered on the Network |++| `OV_OAUTH_CLIENT_SECRET` | — | **Required** for sign-in |++| `OV_OAUTH_REDIRECT_URI` | `https://openvibe.community/auth/callback` | Must match the registration |++| `OV_LIVE_INTERNAL_URL` | `http://127.0.0.1:3000` | Live's `/api/pastes` |++| `OV_LIVE_URL` | `https://openvibe.live` | Author links, avatars, legal pages |++| `OV_MEDIA_URL` | `https://openvibe.media` | Raw text + screenshots |++| `COOKIE_SECURE` | `true` in production | Set `false` for plain-http local dev |++++## Run++++```++npm install++npm start # http://127.0.0.1:4200++npm test # mocks Live + Network in-process; no network needed++```## Deploy```--/opt/openvibe.community/public/index.html # served by nginx root++/opt/openvibe.community # git checkout, `npm ci --omit=dev`++/etc/openvibe/community.env # secrets (0600)++deploy/systemd/openvibe-community.service # → /etc/systemd/system/, User=openvibe, port 4200++deploy/nginx/openvibe.community.conf # → /etc/nginx/sites-available/, TLS from++ # /etc/letsencrypt/live/openvibe.community/```--nginx vhost: `deploy/nginx/openvibe.community.conf` — static root, TLS via the--`openvibe.community` Let's Encrypt wildcard cert.++Update: `git pull && npm ci --omit=dev && systemctl restart openvibe-community`. Nothing is++written to disk by the app; there is no migration step.++++The Network must have the OAuth client `community` registered with redirect++`https://openvibe.community/auth/callback`, and serve the current `openvibe-shared`++`navbar.js` (with `links`/`menu`/`silentLogin` support) for the top links and silent sign-in++to appear.++++## Layout++++```++server/++ index.js process entry (listen, shutdown)++ app.js Express app factory: middleware, routes++ config.js env → config++ auth/routes.js OAuth2 client (login/callback/logout/me/refresh), optionalAuth++ live-client.js server-side reads of Live's /api/pastes for rendered pages++ pastes/proxy.js /api/pastes/* → Live (streams bodies, forwards token + address)++ pastes/catalog.js cached window of recent public pastes: trending, related, language filter++ render/layout.js page shell: SEO head, shared chrome, hashed assets++ render/pages.js home / browse / paste / new / my / error templates++ render/highlight.js highlight.js wrapper, language list, download extensions++ seo.js robots, sitemap, RSS, JSON-LD builders++public/ css/community.css, js/community.js, favicon.svg, og-default.png++vendor/openvibe-shared unmodified copy of OpenVibe.Network/packages/openvibe-shared++deploy/ systemd unit, nginx vhost++test/ run.js + *.test.js (mock Live + mock Network with a real RS256 key)++```++++## What is next (phase 2)++++Spaces (per-streamer / per-topic communities), threads (long-form forum discussion) and++submissions (clips, art, ideas, reports for community review). These need storage of their++own — Community has none today — and moderation tooling; the paste pages, auth layer and++chrome are built to be reused by them.## Related services@@@ -1,22 +1,129 @@++## ═══════════════════════════════════════════════════════════════++## openvibe.community — Nginx Configuration (REFERENCE)++##++## This file is what is deployed to /etc/nginx/sites-available/openvibe.community.conf++## — keep them in sync, and reload nginx after changing it.++##++## The Node app (port 4200, systemd unit openvibe-community) renders every page and proxies++## /api/pastes/* to OpenVibe.Live. nginx terminates TLS, redirects www → apex, rate-limits++## the auth and API paths, and compresses text.++## ═══════════════════════════════════════════════════════════════++++limit_req_zone $binary_remote_addr zone=ovcommunity_api:10m rate=10r/s;++limit_req_zone $binary_remote_addr zone=ovcommunity_auth:10m rate=10r/m;++++# ── HTTP → HTTPS redirect ──────────────────────────────────server {listen 80;listen [::]:80;server_name openvibe.community www.openvibe.community;++++ location /.well-known/acme-challenge/ { root /var/www/certbot; }++ location / { return 301 https://openvibe.community$request_uri; }++}++++# ── www → apex ─────────────────────────────────────────────++server {++ listen 443 ssl;++ listen [::]:443 ssl;++ http2 on;++ server_name www.openvibe.community;++++ ssl_certificate /etc/letsencrypt/live/openvibe.community/fullchain.pem;++ ssl_certificate_key /etc/letsencrypt/live/openvibe.community/privkey.pem;++ ssl_protocols TLSv1.2 TLSv1.3;++return 301 https://openvibe.community$request_uri;}++# ── HTTPS server ───────────────────────────────────────────server {-- listen 443 ssl http2;-- listen [::]:443 ssl http2;-- server_name openvibe.community www.openvibe.community;++ listen 443 ssl;++ listen [::]:443 ssl;++ http2 on;++ server_name openvibe.community;ssl_certificate /etc/letsencrypt/live/openvibe.community/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/openvibe.community/privkey.pem;++ ssl_protocols TLSv1.2 TLSv1.3;++++ access_log /var/log/nginx/openvibe.community.access.log;++ error_log /var/log/nginx/openvibe.community.error.log;-- root /opt/openvibe.community/public;-- index index.html;++ # Text pastes are capped by Media (512 KB by default); screenshots go up to 16 MB there.++ client_max_body_size 2m;++ add_header X-Content-Type-Options nosniff always;++ add_header X-Frame-Options SAMEORIGIN always;++ add_header Referrer-Policy strict-origin-when-cross-origin always;++ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;++++ # Compress text for the hop to Cloudflare as well (the global config only compresses++ # text/html and never proxied responses).++ gzip on;++ gzip_proxied any;++ gzip_vary on;++ gzip_comp_level 5;++ gzip_min_length 1024;++ gzip_types text/css application/javascript text/javascript application/json image/svg+xml text/plain application/xml text/xml application/rss+xml;++++ # ── OAuth session endpoints (strict rate limit) ─────────++ location /auth/ {++ limit_req zone=ovcommunity_auth burst=10 nodelay;++ proxy_pass http://127.0.0.1:4200;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++++ # ── Screenshot uploads (multipart, streamed through to Live → Media) ──++ location = /api/pastes/screenshot {++ limit_req zone=ovcommunity_api burst=5 nodelay;++ client_max_body_size 16m;++ proxy_request_buffering off;++ proxy_read_timeout 120s;++ proxy_send_timeout 120s;++ proxy_pass http://127.0.0.1:4200;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++++ # ── API (moderate rate limit) ───────────────────────────++ location /api/ {++ limit_req zone=ovcommunity_api burst=30 nodelay;++ proxy_pass http://127.0.0.1:4200;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++++ # ── Static assets: Node marks hashed ?v= URLs immutable; let browsers keep them ──++ location ~ ^/(?!api/|auth/).*\.(?:css|js|svg|png|ico|webmanifest|woff2?)$ {++ proxy_pass http://127.0.0.1:4200;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++++ # ── Everything else (server-rendered pages) ─────────────location / {-- try_files $uri $uri/ /index.html;++ proxy_pass http://127.0.0.1:4200;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;++ proxy_set_header X-Forwarded-Proto $scheme;++ proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;++ }++++ location ~ /\.(?!well-known) {++ deny all;}}nnew file mode 100644@@@ -0,0 +1,27 @@++[Unit]++Description=OpenVibe.Community — The people of OpenVibe (pastes; forums and submissions to follow)++# Live (openvibe-live) and Network (openvibe-network) should be up first, but++# Community degrades gracefully if they aren't (paste pages answer 502, sign-in retries).++After=network.target++Wants=openvibe-live.service openvibe-network.service++++[Service]++Type=simple++User=openvibe++Group=openvibe++WorkingDirectory=/opt/openvibe.community++EnvironmentFile=/etc/openvibe/community.env++Environment=NODE_ENV=production++Environment=PORT=4200++Environment=PATH=/usr/local/bin:/usr/bin:/bin++ExecStart=/usr/bin/env node /opt/openvibe.community/server/index.js++Restart=always++RestartSec=5++NoNewPrivileges=true++PrivateTmp=true++ProtectSystem=strict++ProtectHome=true++# Nothing is written to disk — pastes live in OpenVibe.Media, sessions are cookies.++++[Install]++WantedBy=multi-user.target
@@ -1,18 +1,144 @@
@@ -1,18 +1,144 @@
# OpenVibe.Community
# OpenVibe.Community
-Placeholder for the community hub of the OpenVibe network (https://openvibe.community).
+**https://openvibe.community — the people of OpenVibe.**
-The direction for this service is intentionally undecided. For now it is a single static
-page (`public/index.html`) served directly by nginx — there is no application process.
+The community hub of the OpenVibe network: community-run, open source, free speech within
+the rules. Today it is the home of **pastes** (code, text and screenshots with a link);
+spaces, threads and submissions follow.
+
+It is a small Node/Express app (CommonJS, no framework, no database of its own) that
+server-renders every page — crawlers and no-JS readers get the whole thing — and adds a
+little progressive JavaScript for pagination, copy buttons and the upload path.
+
+## How it fits the network
+
+| Concern | Where it lives | How Community reaches it |
+| --- | --- | --- |
+| Paste storage | **OpenVibe.Media** (`pastes` table, `/api/v1/:app/pastes`) | never directly |
+| Paste API + account mapping | **OpenVibe.Live** (`/api/pastes/*`) | `OV_LIVE_INTERNAL_URL` (server-side) |
+| Identity / SSO | **OpenVibe.Network** (OAuth2 + RS256 JWKS) | OAuth client `community` |
+| Raw text, screenshots | OpenVibe.Media public host | 302 from `/p/:slug/raw`, `/p/:slug/screenshot` |
+| Shared chrome, themes | `https://openvibe.network/shared/*.js` | loaded in every page |
+
+Why go through Live and not straight to Media: the visitor's JWT names the account by its
+**Network** id, while the `user_id` Media stores for pastes is **Live's** own — different
+numbers for the same person. Live resolves the visitor against its own accounts before it
+writes, which only Live can do, so Community proxies `/api/pastes/*` to Live and forwards the
+visitor's `Authorization: Bearer <network jwt>` (or `ov_token` cookie). Creation, listing,
+anonymous-post limits, ownership and author names are therefore identical to Live's.
+
+## Endpoints
+
+Pages (server-rendered HTML):
+
+| Route | What |
+| --- | --- |
+| `GET /` | Home: hero, latest pastes, most viewed, "start a paste" CTA, what is coming |
+| `GET /pastes` | Browse — `?q=` search, `?sort=new\|views`, `?lang=`, `?page=` |
+| `GET /p/:slug` | Paste page — highlighted body (server-side, highlight.js), raw/download/copy/fork/share, screenshot, related |
+| `GET /p/:slug/raw` | 302 → `https://openvibe.media/p/:slug/raw` |
+| `GET /p/:slug/screenshot` | 302 → `https://openvibe.media/p/:slug/screenshot` |
+| `GET /p/:slug/download` | The text as an attachment (`slug.ext`); screenshots bounce to Media |
+| `GET /new`, `POST /new` | Create (signed-in or anonymous). `?fork=slug` prefills. The POST is the no-JS fallback; with JS the form talks to `/api/pastes` |
+| `GET /my` | The signed-in user's pastes (public, unlisted and private); anonymous → sign-in |
+
+API and machine endpoints:
+
+| Route | What |
+| --- | --- |
+| `ANY /api/pastes/*` | Transparent proxy to Live `/api/pastes/*` — list, get, create, `screenshot` (multipart), `:slug/copy`, `:slug/like`, comments, delete… bodies stream through untouched |
+| `GET /api/health`, `GET /api/ready` | Liveness / readiness |
+| `GET /auth/login` | → Network `/oauth/authorize`. `?next=` (same-site path, this origin, or `https://openvibe.network/…`), `?silent=1` adds `prompt=none` |
+| `GET /auth/callback` | Code exchange; sets cookies. `error=login_required` → `next` + `?sso=none` |
+| `GET /auth/logout` | Clears session, sets `ov_sso_hint=guest`, honours `?next=` |
+| `GET /auth/me` | Offline-verified profile from `ov_token` |
+| `POST /auth/refresh` | Rotate via refresh token |
+| `GET /robots.txt`, `GET /sitemap.xml`, `GET /feed.xml` | SEO + RSS of the latest pastes |
+
+Cookies are host-only for `openvibe.community`: `ov_token` (24 h access JWT, JS-readable so
+the shared navbar can use it), `ov_refresh` (httpOnly, `/auth`), `ov_sso_hint`
+(`account`/`guest`, 1 year, JS-readable — the navbar only tries a silent sign-in when it says
+`account`).
+
+## SEO
+
+Every page carries a title, description, canonical, robots, Open Graph + Twitter card and
+JSON-LD (`WebSite` on the home page, `Article`/`ImageObject` with author and `datePublished`
+on paste pages, `BreadcrumbList` on both). Unlisted/private pastes and search result pages
+are `noindex`. The sitemap lists home, `/pastes` and the latest public pastes (cached 1 h).
+
+## Configuration
+
+Copy `.env.example` to `.env` (production: `/etc/openvibe/community.env`, mode 0600).
+
+| Variable | Default | Meaning |
+| --- | --- | --- |
+| `PORT` / `HOST` | `4200` / `127.0.0.1` | Listen address (nginx in front) |
+| `BASE_URL` | `https://openvibe.community` in production | Canonical origin |
+| `TRUST_PROXY` | `2` | X-Forwarded-For hops (Cloudflare → nginx → Node) |
+| `OV_NETWORK_URL` | `https://openvibe.network` | Issuer / authorize URL / JWKS |
+| `OV_NETWORK_INTERNAL_URL` | `http://127.0.0.1:4000` | Token grants, JWKS (tried first) |
+| `OV_OAUTH_CLIENT_ID` | `community` | Registered on the Network |
+| `OV_OAUTH_CLIENT_SECRET` | — | **Required** for sign-in |
+| `OV_OAUTH_REDIRECT_URI` | `https://openvibe.community/auth/callback` | Must match the registration |
+| `OV_LIVE_INTERNAL_URL` | `http://127.0.0.1:3000` | Live's `/api/pastes` |
+| `OV_LIVE_URL` | `https://openvibe.live` | Author links, avatars, legal pages |
+| `OV_MEDIA_URL` | `https://openvibe.media` | Raw text + screenshots |
+| `COOKIE_SECURE` | `true` in production | Set `false` for plain-http local dev |
+
+## Run
+
+```
+npm install
+npm start # http://127.0.0.1:4200
+npm test # mocks Live + Network in-process; no network needed
+```
## Deploy
## Deploy
```
```
-/opt/openvibe.community/public/index.html # served by nginx root
+/opt/openvibe.community # git checkout, `npm ci --omit=dev`
+/etc/openvibe/community.env # secrets (0600)
+deploy/systemd/openvibe-community.service # → /etc/systemd/system/, User=openvibe, port 4200
+deploy/nginx/openvibe.community.conf # → /etc/nginx/sites-available/, TLS from
+ # /etc/letsencrypt/live/openvibe.community/
```
```
-nginx vhost: `deploy/nginx/openvibe.community.conf` — static root, TLS via the
-`openvibe.community` Let's Encrypt wildcard cert.
+Update: `git pull && npm ci --omit=dev && systemctl restart openvibe-community`. Nothing is
+written to disk by the app; there is no migration step.
+
+The Network must have the OAuth client `community` registered with redirect
+`https://openvibe.community/auth/callback`, and serve the current `openvibe-shared`
+`navbar.js` (with `links`/`menu`/`silentLogin` support) for the top links and silent sign-in
+to appear.
+
+## Layout
+
+```
+server/
+ index.js process entry (listen, shutdown)
+ app.js Express app factory: middleware, routes
+ config.js env → config
+ auth/routes.js OAuth2 client (login/callback/logout/me/refresh), optionalAuth
+ live-client.js server-side reads of Live's /api/pastes for rendered pages
+ pastes/proxy.js /api/pastes/* → Live (streams bodies, forwards token + address)
+ pastes/catalog.js cached window of recent public pastes: trending, related, language filter
+ render/layout.js page shell: SEO head, shared chrome, hashed assets
+ render/pages.js home / browse / paste / new / my / error templates
+ render/highlight.js highlight.js wrapper, language list, download extensions
+ seo.js robots, sitemap, RSS, JSON-LD builders
+public/ css/community.css, js/community.js, favicon.svg, og-default.png
+vendor/openvibe-shared unmodified copy of OpenVibe.Network/packages/openvibe-shared
+deploy/ systemd unit, nginx vhost
+test/ run.js + *.test.js (mock Live + mock Network with a real RS256 key)
+```
+
+## What is next (phase 2)
+
+Spaces (per-streamer / per-topic communities), threads (long-form forum discussion) and
+submissions (clips, art, ideas, reports for community review). These need storage of their
+own — Community has none today — and moderation tooling; the paste pages, auth layer and
+chrome are built to be reused by them.
## Related services
## Related services
@@ -1,22 +1,129 @@
@@ -1,22 +1,129 @@
+## ═══════════════════════════════════════════════════════════════
+## openvibe.community — Nginx Configuration (REFERENCE)
+##
+## This file is what is deployed to /etc/nginx/sites-available/openvibe.community.conf
+## — keep them in sync, and reload nginx after changing it.
+##
+## The Node app (port 4200, systemd unit openvibe-community) renders every page and proxies
+## /api/pastes/* to OpenVibe.Live. nginx terminates TLS, redirects www → apex, rate-limits
+## the auth and API paths, and compresses text.
+## ═══════════════════════════════════════════════════════════════
+
+limit_req_zone $binary_remote_addr zone=ovcommunity_api:10m rate=10r/s;
+limit_req_zone $binary_remote_addr zone=ovcommunity_auth:10m rate=10r/m;
+
+# ── HTTP → HTTPS redirect ──────────────────────────────────
server {
server {
listen 80;
listen 80;
listen [::]:80;
listen [::]:80;
server_name openvibe.community www.openvibe.community;
server_name openvibe.community www.openvibe.community;
+
+ location /.well-known/acme-challenge/ { root /var/www/certbot; }
+ location / { return 301 https://openvibe.community$request_uri; }
+}
+
+# ── www → apex ─────────────────────────────────────────────
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name www.openvibe.community;
+
+ ssl_certificate /etc/letsencrypt/live/openvibe.community/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/openvibe.community/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
return 301 https://openvibe.community$request_uri;
return 301 https://openvibe.community$request_uri;
}
}
+# ── HTTPS server ───────────────────────────────────────────
server {
server {
- listen 443 ssl http2;
- listen [::]:443 ssl http2;
- server_name openvibe.community www.openvibe.community;
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name openvibe.community;
ssl_certificate /etc/letsencrypt/live/openvibe.community/fullchain.pem;
ssl_certificate /etc/letsencrypt/live/openvibe.community/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/openvibe.community/privkey.pem;
ssl_certificate_key /etc/letsencrypt/live/openvibe.community/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ access_log /var/log/nginx/openvibe.community.access.log;
+ error_log /var/log/nginx/openvibe.community.error.log;
- root /opt/openvibe.community/public;
- index index.html;
+ # Text pastes are capped by Media (512 KB by default); screenshots go up to 16 MB there.
+ client_max_body_size 2m;
+ add_header X-Content-Type-Options nosniff always;
+ add_header X-Frame-Options SAMEORIGIN always;
+ add_header Referrer-Policy strict-origin-when-cross-origin always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+ # Compress text for the hop to Cloudflare as well (the global config only compresses
+ # text/html and never proxied responses).
+ gzip on;
+ gzip_proxied any;
+ gzip_vary on;
+ gzip_comp_level 5;
+ gzip_min_length 1024;
+ gzip_types text/css application/javascript text/javascript application/json image/svg+xml text/plain application/xml text/xml application/rss+xml;
+
+ # ── OAuth session endpoints (strict rate limit) ─────────
+ location /auth/ {
+ limit_req zone=ovcommunity_auth burst=10 nodelay;
+ proxy_pass http://127.0.0.1:4200;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # ── Screenshot uploads (multipart, streamed through to Live → Media) ──
+ location = /api/pastes/screenshot {
+ limit_req zone=ovcommunity_api burst=5 nodelay;
+ client_max_body_size 16m;
+ proxy_request_buffering off;
+ proxy_read_timeout 120s;
+ proxy_send_timeout 120s;
+ proxy_pass http://127.0.0.1:4200;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # ── API (moderate rate limit) ───────────────────────────
+ location /api/ {
+ limit_req zone=ovcommunity_api burst=30 nodelay;
+ proxy_pass http://127.0.0.1:4200;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # ── Static assets: Node marks hashed ?v= URLs immutable; let browsers keep them ──
+ location ~ ^/(?!api/|auth/).*\.(?:css|js|svg|png|ico|webmanifest|woff2?)$ {
+ proxy_pass http://127.0.0.1:4200;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # ── Everything else (server-rendered pages) ─────────────
location / {
location / {
- try_files $uri $uri/ /index.html;
+ proxy_pass http://127.0.0.1:4200;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
+ }
+
+ location ~ /\.(?!well-known) {
+ deny all;
}
}
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
+[Unit]
+Description=OpenVibe.Community — The people of OpenVibe (pastes; forums and submissions to follow)
+# Live (openvibe-live) and Network (openvibe-network) should be up first, but
+# Community degrades gracefully if they aren't (paste pages answer 502, sign-in retries).
+After=network.target
+Wants=openvibe-live.service openvibe-network.service
+
+[Service]
+Type=simple
+User=openvibe
+Group=openvibe
+WorkingDirectory=/opt/openvibe.community
+EnvironmentFile=/etc/openvibe/community.env
+Environment=NODE_ENV=production
+Environment=PORT=4200
+Environment=PATH=/usr/local/bin:/usr/bin:/bin
+ExecStart=/usr/bin/env node /opt/openvibe.community/server/index.js
+Restart=always
+RestartSec=5
+NoNewPrivileges=true
+PrivateTmp=true
+ProtectSystem=strict
+ProtectHome=true
+# Nothing is written to disk — pastes live in OpenVibe.Media, sessions are cookies.
+
+[Install]
+WantedBy=multi-user.target