openvibers/openvibe.design Commits
Open Design messages in Dock Chats (#201)
Changes
@@@ -100,6 +100,13 @@ server {proxy_pass http://127.0.0.1:5090;}++ # The Dock token is fetched on signed-in page views; give it the session probe's budget.++ location = /auth/navbar-token {++ limit_req zone=design_api burst=30 nodelay;++ limit_req_status 429;++ proxy_pass http://127.0.0.1:5090;++ }++location /auth/ {limit_except GET { deny all; }limit_req zone=design_auth burst=10 nodelay;@@@ -183,6 +183,16 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Datreturn res.json({ user: meOf(claims) });});++ // The navbar needs a Network access token for the Dock. Keep /auth/me's++ // profile response token-free; this same-origin answer is never cached.++ r.get('/navbar-token', async (req, res) => {++ res.set('Cache-Control', 'private, no-store');++ const token = sso.extractToken(req);++ const claims = await sso.verify(token);++ if (!claims) return res.status(401).json({ error: 'Not signed in' });++ return res.json({ token, expires_at: claims.exp ? claims.exp * 1000 : null });++ });++// Sign-out stays a same-site action: another site cannot sign a person out.const sameOriginRequest = (req) => {const site = String(req.get('sec-fetch-site') || '');@@@ -61,6 +61,7 @@ function layout({ title, description, canonical, path, robots = 'index, follow',history: { type: 'page', title: title || NAME },silentLogin: `${BASE}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',++ tokenUrl: '/auth/navbar-token',loginUrl: '/auth/login?next={path}',logoutUrl: '/auth/logout?next={path}',};@@@ -84,6 +84,11 @@ const cookieJar = (r) => cookies(r).map((c) => c.split(';')[0]).join('; ');assert.strictEqual(me.status, 200);assert.strictEqual(me.json().user.username, 'kim');assert.ok(!me.text.includes(at.split('=')[1]), '/auth/me never returns the token');++ const nav = await t.get('/auth/navbar-token', { cookie: at });++ assert.strictEqual(nav.status, 200);++ assert.strictEqual(nav.json().token, at.split('=')[1], 'the Dock receives the verified Network token');++ assert.strictEqual(nav.headers.get('cache-control'), 'private, no-store');++ assert.strictEqual((await t.get('/auth/navbar-token')).status, 401, 'a guest receives no token');const tokenRequest = t.network.tokenRequests[t.network.tokenRequests.length - 1];assert.strictEqual(tokenRequest.client_id, 'design');assert.strictEqual(tokenRequest.grant_type, 'authorization_code');
@@ -100,6 +100,13 @@ server {
@@ -100,6 +100,13 @@ server {
proxy_pass http://127.0.0.1:5090;
proxy_pass http://127.0.0.1:5090;
}
}
+ # The Dock token is fetched on signed-in page views; give it the session probe's budget.
+ location = /auth/navbar-token {
+ limit_req zone=design_api burst=30 nodelay;
+ limit_req_status 429;
+ proxy_pass http://127.0.0.1:5090;
+ }
+
location /auth/ {
location /auth/ {
limit_except GET { deny all; }
limit_except GET { deny all; }
limit_req zone=design_auth burst=10 nodelay;
limit_req zone=design_auth burst=10 nodelay;
@@ -183,6 +183,16 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -183,6 +183,16 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
return res.json({ user: meOf(claims) });
return res.json({ user: meOf(claims) });
});
});
+ // The navbar needs a Network access token for the Dock. Keep /auth/me's
+ // profile response token-free; this same-origin answer is never cached.
+ r.get('/navbar-token', async (req, res) => {
+ res.set('Cache-Control', 'private, no-store');
+ const token = sso.extractToken(req);
+ const claims = await sso.verify(token);
+ if (!claims) return res.status(401).json({ error: 'Not signed in' });
+ return res.json({ token, expires_at: claims.exp ? claims.exp * 1000 : null });
+ });
+
// Sign-out stays a same-site action: another site cannot sign a person out.
// Sign-out stays a same-site action: another site cannot sign a person out.
const sameOriginRequest = (req) => {
const sameOriginRequest = (req) => {
const site = String(req.get('sec-fetch-site') || '');
const site = String(req.get('sec-fetch-site') || '');
@@ -61,6 +61,7 @@ function layout({ title, description, canonical, path, robots = 'index, follow',
@@ -61,6 +61,7 @@ function layout({ title, description, canonical, path, robots = 'index, follow',
history: { type: 'page', title: title || NAME },
history: { type: 'page', title: title || NAME },
silentLogin: `${BASE}/auth/login?silent=1&next={url}`,
silentLogin: `${BASE}/auth/login?silent=1&next={url}`,
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}',
};
};
@@ -84,6 +84,11 @@ const cookieJar = (r) => cookies(r).map((c) => c.split(';')[0]).join('; ');
@@ -84,6 +84,11 @@ const cookieJar = (r) => cookies(r).map((c) => c.split(';')[0]).join('; ');
assert.strictEqual(me.status, 200);
assert.strictEqual(me.status, 200);
assert.strictEqual(me.json().user.username, 'kim');
assert.strictEqual(me.json().user.username, 'kim');
assert.ok(!me.text.includes(at.split('=')[1]), '/auth/me never returns the token');
assert.ok(!me.text.includes(at.split('=')[1]), '/auth/me never returns the token');
+ const nav = await t.get('/auth/navbar-token', { cookie: at });
+ assert.strictEqual(nav.status, 200);
+ assert.strictEqual(nav.json().token, at.split('=')[1], 'the Dock receives the verified Network token');
+ assert.strictEqual(nav.headers.get('cache-control'), 'private, no-store');
+ assert.strictEqual((await t.get('/auth/navbar-token')).status, 401, 'a guest receives no token');
const tokenRequest = t.network.tokenRequests[t.network.tokenRequests.length - 1];
const tokenRequest = t.network.tokenRequests[t.network.tokenRequests.length - 1];
assert.strictEqual(tokenRequest.client_id, 'design');
assert.strictEqual(tokenRequest.client_id, 'design');
assert.strictEqual(tokenRequest.grant_type, 'authorization_code');
assert.strictEqual(tokenRequest.grant_type, 'authorization_code');