openvibers/openvibe.mediahub Commits
Open MediaHub messages in Dock Chats (#340)
Changes
@@@ -85,6 +85,13 @@ server {proxy_pass http://127.0.0.1:4990;}++ # Give the Dock token the session probe's rate-limit budget.++ location = /auth/navbar-token {++ limit_req zone=media-hub_api burst=30 nodelay;++ limit_req_status 429;++ proxy_pass http://127.0.0.1:4990;++ }++location /auth/ {limit_req zone=media-hub_auth burst=10 nodelay;limit_req_status 429;@@@ -216,6 +223,13 @@ server {proxy_pass http://127.0.0.1:4990;}++ # Give the Dock token the session probe's rate-limit budget.++ location = /auth/navbar-token {++ limit_req zone=media-hub_api burst=30 nodelay;++ limit_req_status 429;++ proxy_pass http://127.0.0.1:4990;++ }++location /auth/ {limit_req zone=media-hub_auth burst=10 nodelay;limit_req_status 429;@@@ -164,7 +164,7 @@ async function createApp(opts = {}) {// Mounted before the host routing too: both hosts sign in through the same routes, and createSso picks the// callback URI from the Host header (the drive's, or the video host's).// /auth/me is the navbar's who-is-signed-in read on every page, so it is not counted with sign-ins: 60 page views in 15 minutes made a signed-in visitor look signed out.-- app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, skip: (req) => req.path === '/me', standardHeaders: true, legacyHeaders: false }));++ app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, skip: (req) => req.path === '/me' || req.path === '/navbar-token', standardHeaders: true, legacyHeaders: false }));app.use('/auth', sso.routes());// /release.json (and loopback /metrics) answer on both hosts: the shared release check on openvibe.video reads its own@@@ -216,6 +216,14 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Datreturn res.json({ user: { username: v.username, display_name: v.displayName, subject_id: v.subject, role: v.role, avatar_url: v.avatarUrl || null, profile_color: v.profileColor || null } });});++ // The shared Dock needs a verified Network access token from this HTTP-only session.++ r.get('/navbar-token', (req, res) => {++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));++ const viewer = req.viewer;++ if (!viewer || viewer.kind !== 'user' || !viewer.token) return res.status(401).json({ error: 'Not signed in' });++ return res.json({ token: viewer.token });++ });++r.get('/logout', (req, res, next) => {if (sameOriginRequest(req)) return next();res.set('Cache-Control', cache.htmlHeaders({ private: true }));@@@ -92,6 +92,7 @@ ${render(o.body)}links: NAV_LINKS.map((l) => ({ ...l, active: l.href === '/' ? o.path === '/' : String(o.path || '').startsWith(l.href) })),history: { type: 'page', title },sessionUrl: '/auth/me',++ tokenUrl: '/auth/navbar-token',loginUrl: '/auth/login?next={path}',logoutUrl: '/auth/logout?next={path}',silentLogin: `${String(o.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,@@@ -65,6 +65,7 @@ function renderPage(o) {history: { type: 'page', title: o.title || SITE_NAME },silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',++ tokenUrl: '/auth/navbar-token',loginUrl: '/auth/login?next={path}', // filled from the current page (boost moves between pages)logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session toonotificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)@@@ -104,6 +104,7 @@ ${render(o.body)}search: { page: '/search', label: 'OpenVibe.Video' },history: { type: 'page', title },sessionUrl: '/auth/me',++ tokenUrl: '/auth/navbar-token',loginUrl: '/auth/login?next={path}',logoutUrl: '/auth/logout?next={path}',silentLogin: `${String(o.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,@@@ -77,6 +77,14 @@ const { signJwt } = require('./helpers/mocks');assert.strictEqual(out.headers.get('clear-site-data'), null);});++ await check('the navbar receives a verified, uncached Network token only for a signed-in person', async () => {++ const nav = await s.get('/auth/navbar-token', { as: staff });++ assert.strictEqual(nav.status, 200);++ assert.match(JSON.parse(nav.text).token, /^[^.]+\.[^.]+\.[^.]+$/);++ assert.strictEqual(nav.headers.get('cache-control'), 'private, no-store');++ assert.strictEqual((await s.get('/auth/navbar-token')).status, 401);++ });++await check('another site cannot sign a person out; this site can', async () => {const cross = await s.get('/auth/logout', { as: staff, headers: { 'sec-fetch-site': 'cross-site' } });assert.strictEqual(cross.status, 200, 'a confirm page, not a sign-out');
@@ -85,6 +85,13 @@ server {
@@ -85,6 +85,13 @@ server {
proxy_pass http://127.0.0.1:4990;
proxy_pass http://127.0.0.1:4990;
}
}
+ # Give the Dock token the session probe's rate-limit budget.
+ location = /auth/navbar-token {
+ limit_req zone=media-hub_api burst=30 nodelay;
+ limit_req_status 429;
+ proxy_pass http://127.0.0.1:4990;
+ }
+
location /auth/ {
location /auth/ {
limit_req zone=media-hub_auth burst=10 nodelay;
limit_req zone=media-hub_auth burst=10 nodelay;
limit_req_status 429;
limit_req_status 429;
@@ -216,6 +223,13 @@ server {
@@ -216,6 +223,13 @@ server {
proxy_pass http://127.0.0.1:4990;
proxy_pass http://127.0.0.1:4990;
}
}
+ # Give the Dock token the session probe's rate-limit budget.
+ location = /auth/navbar-token {
+ limit_req zone=media-hub_api burst=30 nodelay;
+ limit_req_status 429;
+ proxy_pass http://127.0.0.1:4990;
+ }
+
location /auth/ {
location /auth/ {
limit_req zone=media-hub_auth burst=10 nodelay;
limit_req zone=media-hub_auth burst=10 nodelay;
limit_req_status 429;
limit_req_status 429;
@@ -164,7 +164,7 @@ async function createApp(opts = {}) {
@@ -164,7 +164,7 @@ async function createApp(opts = {}) {
// Mounted before the host routing too: both hosts sign in through the same routes, and createSso picks the
// Mounted before the host routing too: both hosts sign in through the same routes, and createSso picks the
// callback URI from the Host header (the drive's, or the video host's).
// callback URI from the Host header (the drive's, or the video host's).
// /auth/me is the navbar's who-is-signed-in read on every page, so it is not counted with sign-ins: 60 page views in 15 minutes made a signed-in visitor look signed out.
// /auth/me is the navbar's who-is-signed-in read on every page, so it is not counted with sign-ins: 60 page views in 15 minutes made a signed-in visitor look signed out.
- app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, skip: (req) => req.path === '/me', standardHeaders: true, legacyHeaders: false }));
+ app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, skip: (req) => req.path === '/me' || req.path === '/navbar-token', standardHeaders: true, legacyHeaders: false }));
app.use('/auth', sso.routes());
app.use('/auth', sso.routes());
// /release.json (and loopback /metrics) answer on both hosts: the shared release check on openvibe.video reads its own
// /release.json (and loopback /metrics) answer on both hosts: the shared release check on openvibe.video reads its own
@@ -216,6 +216,14 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -216,6 +216,14 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
return res.json({ user: { username: v.username, display_name: v.displayName, subject_id: v.subject, role: v.role, avatar_url: v.avatarUrl || null, profile_color: v.profileColor || null } });
return res.json({ user: { username: v.username, display_name: v.displayName, subject_id: v.subject, role: v.role, avatar_url: v.avatarUrl || null, profile_color: v.profileColor || null } });
});
});
+ // The shared Dock needs a verified Network access token from this HTTP-only session.
+ r.get('/navbar-token', (req, res) => {
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
+ const viewer = req.viewer;
+ if (!viewer || viewer.kind !== 'user' || !viewer.token) return res.status(401).json({ error: 'Not signed in' });
+ return res.json({ token: viewer.token });
+ });
+
r.get('/logout', (req, res, next) => {
r.get('/logout', (req, res, next) => {
if (sameOriginRequest(req)) return next();
if (sameOriginRequest(req)) return next();
res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.set('Cache-Control', cache.htmlHeaders({ private: true }));
@@ -92,6 +92,7 @@ ${render(o.body)}
@@ -92,6 +92,7 @@ ${render(o.body)}
links: NAV_LINKS.map((l) => ({ ...l, active: l.href === '/' ? o.path === '/' : String(o.path || '').startsWith(l.href) })),
links: NAV_LINKS.map((l) => ({ ...l, active: l.href === '/' ? o.path === '/' : String(o.path || '').startsWith(l.href) })),
history: { type: 'page', title },
history: { type: 'page', title },
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
+ tokenUrl: '/auth/navbar-token',
loginUrl: '/auth/login?next={path}',
loginUrl: '/auth/login?next={path}',
logoutUrl: '/auth/logout?next={path}',
logoutUrl: '/auth/logout?next={path}',
silentLogin: `${String(o.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,
silentLogin: `${String(o.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,
@@ -65,6 +65,7 @@ function renderPage(o) {
@@ -65,6 +65,7 @@ function renderPage(o) {
history: { type: 'page', title: o.title || SITE_NAME },
history: { type: 'page', title: o.title || SITE_NAME },
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
+ tokenUrl: '/auth/navbar-token',
loginUrl: '/auth/login?next={path}', // filled from the current page (boost moves between pages)
loginUrl: '/auth/login?next={path}', // filled from the current page (boost moves between pages)
logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
@@ -104,6 +104,7 @@ ${render(o.body)}
@@ -104,6 +104,7 @@ ${render(o.body)}
search: { page: '/search', label: 'OpenVibe.Video' },
search: { page: '/search', label: 'OpenVibe.Video' },
history: { type: 'page', title },
history: { type: 'page', title },
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
+ tokenUrl: '/auth/navbar-token',
loginUrl: '/auth/login?next={path}',
loginUrl: '/auth/login?next={path}',
logoutUrl: '/auth/logout?next={path}',
logoutUrl: '/auth/logout?next={path}',
silentLogin: `${String(o.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,
silentLogin: `${String(o.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,
@@ -77,6 +77,14 @@ const { signJwt } = require('./helpers/mocks');
@@ -77,6 +77,14 @@ const { signJwt } = require('./helpers/mocks');
assert.strictEqual(out.headers.get('clear-site-data'), null);
assert.strictEqual(out.headers.get('clear-site-data'), null);
});
});
+ await check('the navbar receives a verified, uncached Network token only for a signed-in person', async () => {
+ const nav = await s.get('/auth/navbar-token', { as: staff });
+ assert.strictEqual(nav.status, 200);
+ assert.match(JSON.parse(nav.text).token, /^[^.]+\.[^.]+\.[^.]+$/);
+ assert.strictEqual(nav.headers.get('cache-control'), 'private, no-store');
+ assert.strictEqual((await s.get('/auth/navbar-token')).status, 401);
+ });
+
await check('another site cannot sign a person out; this site can', async () => {
await check('another site cannot sign a person out; this site can', async () => {
const cross = await s.get('/auth/logout', { as: staff, headers: { 'sec-fetch-site': 'cross-site' } });
const cross = await s.get('/auth/logout', { as: staff, headers: { 'sec-fetch-site': 'cross-site' } });
assert.strictEqual(cross.status, 200, 'a confirm page, not a sign-out');
assert.strictEqual(cross.status, 200, 'a confirm page, not a sign-out');