openvibers/openvibe.shared Commits

Profile panel: data-ov-profile="me" shows whoever is signed in on the site (its /auth/me), so cached pages can show your stuff (#115)

OP OpenVibers committed 19 days ago · 7a96052fed · 4 files, +38 −6

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex fd5a548..6306f52 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -10,7 +10,8 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
draws someone's worn items, their counts of items, badges, achievements and games, their latest unlock per game and
links to their profile and inventory, from OpenVibe.Network's public profile (which gathers Inventory, Quest and
Play since Network#333 and Play#38). Steam-style, so Games, Quest and any site can show it (the owner, 2026-10-25).
-- `test/profile-panel.test.js`.
++ `data-ov-profile="me"` is whoever is signed in on that site (its own `/auth/me`), so a page cached for everyone can
++ show "your stuff" (a `hidden` element appears once drawn). `test/profile-panel.test.js`.
- **One lit row in the menus on phones** (`navbar.js`). A tap left `:hover` on the row it touched, so the "all sites"
launcher showed two highlighted rows: the site you are on and the last one your finger crossed (the owner, 2026-10-25,
on openre.stream). The launcher's and the phone drawer's hover backgrounds now apply only under `@media (hover:
ddiff --git a/README.md b/README.md
iindex 3666467..23b5df5 100644
---- a/README.md
++++ b/README.md
@@@ -710,7 +710,8 @@ able to see your inventory anywhere"). Put a fallback link in an element and loa
```
Every `[data-ov-profile]` on the page is drawn once loaded; `OpenVibeProfilePanel.render(el, username)` draws one added
--later. It reads OpenVibe.Network's public profile (`GET https://openvibe.network/api/v1/profiles/:username`, CORS
++later. `data-ov-profile="me"` is whoever is signed in on that site (its own `/auth/me`, the navbar's session lookup), so a
++page cached for everyone can still show "your stuff": give the element `hidden` and it appears only once drawn. It reads OpenVibe.Network's public profile (`GET https://openvibe.network/api/v1/profiles/:username`, CORS
open, no credentials), which gathers OpenVibe.Inventory, OpenVibe.Quest and OpenVibe.Play. Values are text, links only
to OpenVibe's own https hosts, pictures only from openvibe.media and openvibe.network; a private profile shows its name
and picture; no answer leaves the fallback link. `window.OpenVibeProfilePanelConfig = { api }` points it at another
ddiff --git a/profile-panel.js b/profile-panel.js
iindex 83f30f2..cc3189b 100644
---- a/profile-panel.js
++++ b/profile-panel.js
@@@ -6,6 +6,9 @@
* <div data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana's profile</a></div>
* <script src="/shared/profile-panel.js" defer></script>
* OpenVibeProfilePanel.render(el, 'ana') or every [data-ov-profile] on the page, by itself, once loaded
++ * <section data-ov-profile="me" hidden>…</section> whoever is signed in on this site (its own /auth/me, the navbar's
++ * session lookup), so a cached page can show "your stuff"; shown
++ * only once drawn, left as it was for someone signed out
*
* It reads OpenVibe.Network's public profile (GET https://openvibe.network/api/v1/profiles/:username: profile@1, CORS
* open, no credentials, cached 60 s by the Network), which gathers Inventory, Quest and Play. The panel replaces the
@@@ -91,6 +94,7 @@
if (p.private) {
card.appendChild(el('p', 'ovpp-note', 'This profile is private.'));
target.replaceChildren(card);
++ target.hidden = false;
return card;
}
const ach = p.achievements || { total: 0, games: [] };
@@@ -143,13 +147,29 @@
if (p.inventory_url) { foot.appendChild(root.document.createTextNode(' \u00b7 ')); foot.appendChild(link(p.inventory_url, null, 'Inventory')); }
card.appendChild(foot);
target.replaceChildren(card);
++ target.hidden = false;
return card;
}
/** Draw `username`'s panel into `target`. → the panel, or null (the fallback stays) when there is no answer. */
++ /** The signed-in person's username from this site's own /auth/me (the navbar's session lookup), once a page. */
++ let mePromise = null;
++ function me() {
++ if (!mePromise) {
++ mePromise = fetch('/auth/me', { credentials: 'same-origin', headers: { Accept: 'application/json' } })
++ .then((r) => (r.ok ? r.json() : null)).then((b) => {
++ const u = b && b.user;
++ const name = u && (u.username || u.name);
++ return NAME.test(String(name || '')) ? String(name) : null;
++ }).catch(() => null);
++ }
++ return mePromise;
++ }
++
async function render(target, username) {
-- const name = String(username || '');
-- if (!target || !NAME.test(name)) return null;
++ // "me": whoever is signed in on this site; nobody signed in leaves the element as it was (hidden stays hidden).
++ const name = String(username || '') === 'me' ? await me() : String(username || '');
++ if (!target || !NAME.test(String(name || ''))) return null;
const p = await load(name);
return p ? draw(target, p) : null;
}
ddiff --git a/test/profile-panel.test.js b/test/profile-panel.test.js
iindex 29f536a..d72e3be 100644
---- a/test/profile-panel.test.js
++++ b/test/profile-panel.test.js
@@@ -2,7 +2,8 @@
// profile-panel.js in a real headless Chrome at phone width: [data-ov-profile] elements become a person's stuff from
// the Network's public profile JSON (worn items, counts, achievements by game, badges, links); every value is text
// (a name with markup stays text), only https links on OpenVibe's own hosts become hrefs, a private profile shows its
--// name only, a missing one leaves the fallback link, and nothing on the panel widens the page past the phone.
++// name only, a missing one leaves the fallback link, "me" is whoever the site's /auth/me names (a hidden element shows
++// once drawn), and nothing on the panel widens the page past the phone.
// Skipped (exit 0) without Chrome or with OV_SKIP_BROWSER=1.
const assert = require('assert');
const fs = require('fs');
@@@ -32,6 +33,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
<body style="margin:0;padding:16px"><div id="ana" data-ov-profile="ana"><a href="https://openvibe.network/@ana">Ana</a></div>
<div id="priv" data-ov-profile="hidden"><a href="https://openvibe.network/@hidden">Hidden</a></div>
<div id="none" data-ov-profile="nobody"><a href="https://openvibe.network/@nobody">Nobody</a></div>
++<section id="me" data-ov-profile="me" hidden><a href="https://openvibe.network/my">Your stuff</a></section>
<script>window.OpenVibeProfilePanelConfig = { api: location.origin };</script>
<script src="/shared/profile-panel.js" defer></script></body></html>`;
const server = http.createServer((req, res) => {
@@@ -39,6 +41,9 @@ const server = http.createServer((req, res) => {
const json = (s, o) => { res.writeHead(s, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
if (u.pathname === '/') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE); }
if (u.pathname === '/api/v1/profiles/ana') return json(200, { profile: ANA });
++ // Signed in on the main page; signed out on /out (told apart by the page that asks).
++ if (u.pathname === '/auth/me') return json(200, { user: /\/out$/.test(req.headers.referer || '') ? null : { id: 7, username: 'ana' } });
++ if (u.pathname === '/out') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE.replace(/<div id="ana"[\s\S]*?<\/section>/, '<section id="me" data-ov-profile="me" hidden><a href="https://openvibe.network/my">Your stuff</a></section>')); }
if (u.pathname === '/api/v1/profiles/hidden') return json(200, { profile: { username: 'hidden', display_name: 'Hidden', profile_url: 'https://openvibe.network/@hidden', private: true } });
if (u.pathname.startsWith('/api/v1/profiles/')) return json(404, { error: 'not_found' });
if (u.pathname.startsWith('/avatar/')) { res.writeHead(204); return res.end(); }
@@@ -56,7 +61,7 @@ const server = http.createServer((req, res) => {
const context = await browser.newContext();
const p = await h.openPage(browser, context, { width: 360, height: 900 });
await p.goto(`${base}/`, { maxMs: 5000 });
-- const got = JSON.parse(await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('#ana .ovpp') && document.querySelector('#priv .ovpp')) { clearInterval(t); setTimeout(() => {
++ const got = JSON.parse(await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('#ana .ovpp') && document.querySelector('#priv .ovpp') && document.querySelector('#me .ovpp')) { clearInterval(t); setTimeout(() => {
const ana = document.querySelector('#ana .ovpp');
r(JSON.stringify({
name: ana.querySelector('.ovpp-who b').textContent,
@@@ -70,6 +75,7 @@ const server = http.createServer((req, res) => {
priv: document.querySelector('#priv .ovpp').textContent,
none: document.getElementById('none').innerHTML,
wide: document.documentElement.scrollWidth,
++ me: [document.getElementById('me').hidden, document.querySelector('#me .ovpp-who small').textContent],
}));
}, 100); } }, 30); })`));
assert.strictEqual(got.name, 'Ana <img src=x onerror=alert(1)>', 'a name with markup stays text');
@@@ -83,6 +89,10 @@ const server = http.createServer((req, res) => {
assert.match(got.priv, /Hidden[\s\S]*This profile is private\./);
assert.match(got.none, /href="https:\/\/openvibe\.network\/@nobody"/, 'no profile: the fallback link stays');
assert.ok(got.wide <= 360, `fits a 360 px phone (${got.wide})`);
++ assert.deepStrictEqual(got.me, [false, '@ana'], '"me" is whoever /auth/me names, shown once drawn');
++ await p.goto(`${base}/out`, { maxMs: 5000 });
++ const out = await p.evaluate(`new Promise((r) => setTimeout(() => r(JSON.stringify([document.getElementById('me').hidden, !!document.querySelector('#me .ovpp')])), 700))`);
++ assert.deepStrictEqual(JSON.parse(out), [true, false], 'signed out: "me" stays hidden and undrawn');
console.log('profile-panel: ok');
} finally {
await browser.close();