openvibers/openvibe.live Commits

Sessions: silent SSO re-sign-in via openvibe.network (prompt=none), Switch account + Browse as guest in the user menu, quiet fallback when the network has no session; OV mark drop-in used in footer, static pages, kiosk and tour core

OP OpenVibers committed 16 Sept 2026 · d1b539f13c · 9 files, +113 −10

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/public/dmca.html b/public/dmca.html
iindex 71eaf11a..38274aa0 100644
---- a/public/dmca.html
++++ b/public/dmca.html
@@@ -200,7 +200,7 @@
<header class="topbar">
<a class="topbar-brand" href="https://openvibe.live">
-- <i class="fa-solid fa-circle-nodes"></i>
++ <span class="ov-mark" data-size="28"></span>
<span><span class="openvibe">OpenVibe</span>.Live</span>
</a>
<span class="topbar-sep">/</span>
@@@ -345,5 +345,6 @@
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
</footer>
++<script src="/js/ov-mark.js?v=1"></script>
</body>
</html>
ddiff --git a/public/index.html b/public/index.html
iindex 67f82add..34bd5b4e 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -205,6 +205,8 @@
<a href="#" onclick="event.preventDefault();captureScreenshot()"><i class="fa-solid fa-camera"></i> Screenshot</a>
<a href="/documentation" onclick="return handleLinkClick(event, '/documentation')"><i class="fa-solid fa-book"></i> Documentation</a>
<hr>
++ <a href="#" onclick="event.preventDefault();switchAccount()"><i class="fa-solid fa-people-arrows"></i> Switch account</a>
++ <a href="#" onclick="event.preventDefault();browseAsGuest()"><i class="fa-solid fa-user-secret"></i> Browse as guest</a>
<a href="#" onclick="event.preventDefault();logout()"><i class="fa-solid fa-right-from-bracket"></i> Logout</a>
</div>
</div>
@@@ -5038,7 +5040,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<!-- ═══════════════════════════════════════════════════════════ -->
<footer class="footer">
<div class="footer-content">
-- <span class="footer-brand"><i class="fa-solid fa-circle-nodes"></i> OpenVibe.Live</span>
++ <span class="footer-brand"><span class="ov-mark" data-size="22"></span> OpenVibe.Live</span>
<span class="footer-divider">&middot;</span>
<a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank"><i class="fa-brands fa-github"></i></a>
<a href="https://discord.gg/M6MuRUaeJj" target="_blank"><i class="fa-brands fa-discord"></i></a>
@@@ -5086,7 +5088,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<!-- Scripts -->
<script src="/js/theme-engine.js?v=4"></script>
<script src="/live-notify.js?v=3" defer></script>
--<script src="/js/app.js?v=139" defer></script>
++<script src="/js/app.js?v=140" defer></script>
<script src="/js/stream-player.js?v=21" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
@@@ -5165,8 +5167,9 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/pastes.js?v=15" defer></script>
<script src="/js/draggable-fab.js?v=2" defer></script>
<script src="/js/messenger.js?v=2" defer></script>
++<script src="/js/ov-mark.js?v=1" defer></script>
<script src="/js/home-fx.js?v=3" defer></script>
--<script src="/js/home-tour.js?v=3" defer></script>
++<script src="/js/home-tour.js?v=4" defer></script>
<script src="/js/broadcast-guide.js?v=1" defer></script>
<!-- ── OpenVibe shared libs (notifications, account sync, themes) ── -->
ddiff --git a/public/js/app.js b/public/js/app.js
iindex 4746dcc2..1e701985 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -680,7 +680,31 @@ function closeModal() {
function doLogin() { window.location.href = '/api/auth/sso/login'; }
function doRegister() { window.location.href = '/api/auth/sso/login'; }
++// Session hint: 'account' after a successful sign-in (silent SSO may re-sign you in when the
++// site token lapses), 'guest' after an explicit logout / "browse as guest" (never re-sign in).
++function _setSsoHint(v) { try { localStorage.setItem('ov_sso_hint', v); } catch { /* */ } }
++function _ssoHint() { try { return localStorage.getItem('ov_sso_hint') || ''; } catch { return ''; } }
++/** Try to sign back in through openvibe.network without a chooser — once per tab session. */
++function _trySilentSso() {
++ if (_ssoHint() !== 'account') return false;
++ try { if (sessionStorage.getItem('ov_silent_sso')) return false; sessionStorage.setItem('ov_silent_sso', '1'); } catch { return false; }
++ if (location.pathname.startsWith('/banned') || location.pathname.startsWith('/api/')) return false;
++ location.href = '/api/auth/sso/login?silent=1';
++ return true;
++}
++/** "Switch account": drop this site's session, keep openvibe.network's, open the account chooser. */
++function switchAccount() {
++ fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}).finally(() => {
++ localStorage.removeItem('token'); localStorage.removeItem('ov_token');
++ document.cookie = 'token=;Max-Age=0;path=/'; document.cookie = 'ov_token=;Max-Age=0;path=/';
++ _setSsoHint('account');
++ location.href = '/api/auth/sso/login';
++ });
++}
++/** "Browse as guest": signed out here only — openvibe.network still remembers you, so "Sign in" is one tap. */
++function browseAsGuest() { logout(); try { toast('Browsing as a guest — Sign in brings you straight back.', 'info'); } catch { /* */ } }
function logout() {
++ _setSsoHint('guest');
// Clear server-side cookies via API
fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
// Clear client-side storage (both keys)
@@@ -706,7 +730,7 @@ async function loadUser() {
if (!tok) {
// No token in localStorage — try refreshing from httpOnly cookie
const refreshed = await tryRefreshToken();
-- if (!refreshed) return;
++ if (!refreshed) { _trySilentSso(); return; }
tok = localStorage.getItem('token');
}
// If token is already expired, proactively refresh before making the API call
@@@ -717,17 +741,22 @@ async function loadUser() {
try {
const data = await api('/auth/me');
currentUser = mergeUserWithCapabilities(data.user || data, data.capabilities);
++ _setSsoHint('account');
} catch (err) {
// Banned account: the server refuses with 403 — show the ban screen instead of the site.
if (err && err.status === 403 && /banned/i.test(String(err.message || ''))) {
if (!location.pathname.startsWith('/banned')) location.replace('/banned');
return;
}
-- // If still 401 after auto-refresh attempt in api(), give up
++ // If still 401 after auto-refresh attempt in api(), give up here — but let the network
++ // sign us back in silently if it still has a session for this browser.
localStorage.removeItem('token');
localStorage.removeItem('ov_token');
++ _trySilentSso();
}
}
++// Back from a silent sign-in that found no network session: stay a guest, tidy the URL.
++try { if (new URLSearchParams(location.search).get('sso') === 'none') { _setSsoHint('guest'); history.replaceState(null, '', location.pathname); } } catch { /* */ }
function onAuthChange() {
const anon = document.getElementById('nav-auth-anon');
ddiff --git a/public/js/home-tour.js b/public/js/home-tour.js
iindex a4576077..aa9aaabe 100644
---- a/public/js/home-tour.js
++++ b/public/js/home-tour.js
@@@ -50,7 +50,7 @@
<div class="tour-core-wrap">
<div class="tour-core" data-node="core">
<span class="tour-core-ring"></span><span class="tour-core-ring tour-core-ring--2"></span>
-- <span class="tour-core-logo"><i class="fa-solid fa-circle-nodes"></i></span>
++ <span class="tour-core-logo"><span class="ov-mark" data-size="30" style="color:#fff"></span></span>
<b>OpenVibe.Live</b>
<small>&lt;1s latency · chat · emotes · sounds · controls · VOD · clips</small>
</div>
ddiff --git a/public/js/ov-mark.js b/public/js/ov-mark.js
nnew file mode 100644
iindex 00000000..2c018cf5
---- /dev/null
++++ b/public/js/ov-mark.js
@@@ -0,0 +1,59 @@
++/**
++ * ov-mark.js — the OpenVibe "OV" brand mark as a drop-in.
++ * <span class="ov-mark"></span> → 32px animated mark
++ * <span class="ov-mark" data-size="56"> → any size; add data-static="1" for no motion
++ * Self-contained: injects its own CSS once, works on any page (Live, Network, static pages).
++ * The O is a ring with a comet arc running around it, the V is drawn inside with a light
++ * sweep, and a dot orbits the ring. Transform/opacity/stroke motion only.
++ */
++(function () {
++ 'use strict';
++ if (window.__ovMark) return; window.__ovMark = true;
++ let n = 0;
++ const CSS = `
++.ov-mark{display:inline-grid;place-items:center;width:32px;height:32px;color:var(--accent,#8b5cf6);flex:none;vertical-align:middle;line-height:0}
++.ov-mark svg{width:100%;height:100%;overflow:visible;transform-origin:50% 50%;animation:ovmFloat 5s ease-in-out infinite}
++.ov-mark .g{fill:currentColor;opacity:.1;transform-box:fill-box;transform-origin:center;animation:ovmGlow 3.4s ease-in-out infinite}
++.ov-mark .r{fill:none;stroke:currentColor;stroke-width:4;opacity:.28}
++.ov-mark .c{fill:none;stroke-width:4;stroke-linecap:round;stroke-dasharray:34 79;transform-box:fill-box;transform-origin:center;animation:ovmComet 3.6s linear infinite}
++.ov-mark .v{fill:none;stroke-width:4.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:46;animation:ovmDraw 1.1s cubic-bezier(.2,.8,.2,1) both}
++.ov-mark .d{fill:#fff;transform-box:fill-box;transform-origin:center;animation:ovmDot 2.4s ease-in-out infinite}
++.ov-mark .o{fill:#fff;filter:drop-shadow(0 0 3px currentColor)}
++.ov-mark .o2{opacity:.5}
++.ov-mark:hover svg,a:hover>.ov-mark svg,button:hover>.ov-mark svg{animation:ovmSpin .9s cubic-bezier(.2,1.5,.3,1) 1}
++.ov-mark:hover .c{animation-duration:.9s}
++.ov-mark[data-static] svg,.ov-mark[data-static] .g,.ov-mark[data-static] .c,.ov-mark[data-static] .v,.ov-mark[data-static] .d{animation:none!important}
++.ov-mark[data-static] .o{display:none}
++@keyframes ovmFloat{0%,100%{transform:translateY(0) rotate(0deg)}50%{transform:translateY(-1px) rotate(2deg)}}
++@keyframes ovmGlow{0%,100%{opacity:.08;transform:scale(.92)}50%{opacity:.2;transform:scale(1.06)}}
++@keyframes ovmComet{to{transform:rotate(360deg)}}
++@keyframes ovmDraw{from{stroke-dashoffset:46}to{stroke-dashoffset:0}}
++@keyframes ovmDot{0%,100%{transform:scale(1);opacity:.85}50%{transform:scale(1.5);opacity:1}}
++@keyframes ovmSpin{from{transform:rotate(0deg) scale(1)}40%{transform:rotate(200deg) scale(1.18)}to{transform:rotate(360deg) scale(1)}}
++@media (prefers-reduced-motion:reduce){.ov-mark svg,.ov-mark .g,.ov-mark .c,.ov-mark .v,.ov-mark .d{animation:none!important}.ov-mark .o{display:none}.ov-mark .c{stroke-dasharray:none;opacity:.9}}`;
++ function svg(id) {
++ return `<svg viewBox="0 0 48 48" aria-hidden="true"><defs>
++<linearGradient id="${id}g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor"/><stop offset="1" stop-color="currentColor" stop-opacity="0.45"/></linearGradient>
++<linearGradient id="${id}v" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="currentColor"/><stop offset="1" stop-color="#fff" stop-opacity="0.9"/><animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="3.2s" repeatCount="indefinite"/></linearGradient>
++<path id="${id}p" d="M24,6 A18,18 0 1,1 23.99,6 Z"/></defs>
++<circle class="g" cx="24" cy="24" r="21"/><circle class="r" cx="24" cy="24" r="18"/><circle class="c" cx="24" cy="24" r="18" stroke="url(#${id}g)"/>
++<path class="v" d="M14.5,17 L24,34 L33.5,17" stroke="url(#${id}v)"/><circle class="d" cx="24" cy="34" r="2.6"/>
++<circle class="o" r="2"><animateMotion dur="3.6s" repeatCount="indefinite"><mpath href="#${id}p"/></animateMotion></circle>
++<circle class="o o2" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#${id}p"/></animateMotion></circle></svg>`;
++ }
++ function mount(root) {
++ (root || document).querySelectorAll('.ov-mark:not([data-ov])').forEach(el => {
++ el.setAttribute('data-ov', '1');
++ const size = parseInt(el.getAttribute('data-size'), 10);
++ if (size) { el.style.width = size + 'px'; el.style.height = size + 'px'; }
++ el.innerHTML = svg('ovm' + (++n) + '_');
++ });
++ }
++ function init() {
++ if (!document.getElementById('ov-mark-css')) { const st = document.createElement('style'); st.id = 'ov-mark-css'; st.textContent = CSS; document.head.appendChild(st); }
++ mount();
++ try { new MutationObserver(() => mount()).observe(document.body, { childList: true, subtree: true }); } catch { /* */ }
++ }
++ window.ovMarkMount = mount;
++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();
++})();
ddiff --git a/public/kiosk.html b/public/kiosk.html
iindex 9f7adb9a..7ab9fab1 100644
---- a/public/kiosk.html
++++ b/public/kiosk.html
@@@ -519,7 +519,7 @@ button{font:inherit}
<div class="page">
<header class="topbar">
<a class="brand" href="https://openvibe.live">
-- <span class="logo"><i class="fa-solid fa-circle-nodes"></i></span>
++ <span class="logo"><span class="ov-mark" data-size="26"></span></span>
<span>OPENVIBE NETWORK</span>
</a>
<div class="network-line">
@@@ -1259,5 +1259,6 @@ button{font:inherit}
window.addEventListener("hashchange",maybeAutoOpen);
})();
</script>
++<script src="/js/ov-mark.js?v=1"></script>
</body>
</html>
ddiff --git a/public/privacy.html b/public/privacy.html
iindex e7a970fb..a4bf2568 100644
---- a/public/privacy.html
++++ b/public/privacy.html
@@@ -196,7 +196,7 @@
<header class="topbar">
<a class="topbar-brand" href="https://openvibe.live">
-- <i class="fa-solid fa-circle-nodes"></i>
++ <span class="ov-mark" data-size="28"></span>
<span><span class="openvibe">OpenVibe</span>.Live</span>
</a>
<span class="topbar-sep">/</span>
@@@ -322,5 +322,6 @@
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
</footer>
++<script src="/js/ov-mark.js?v=1"></script>
</body>
</html>
ddiff --git a/public/tos.html b/public/tos.html
iindex 488b0ee6..04a0e37f 100644
---- a/public/tos.html
++++ b/public/tos.html
@@@ -196,7 +196,7 @@
<header class="topbar">
<a class="topbar-brand" href="https://openvibe.live">
-- <i class="fa-solid fa-circle-nodes"></i>
++ <span class="ov-mark" data-size="28"></span>
<span><span class="openvibe">OpenVibe</span>.Live</span>
</a>
<span class="topbar-sep">/</span>
@@@ -359,5 +359,6 @@
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
</footer>
++<script src="/js/ov-mark.js?v=1"></script>
</body>
</html>
ddiff --git a/server/auth/routes.js b/server/auth/routes.js
iindex 24f42334..46c26d14 100644
---- a/server/auth/routes.js
++++ b/server/auth/routes.js
@@@ -301,6 +301,9 @@ router.get('/sso/login', (req, res) => {
scope: 'profile theme',
state,
});
++ // ?silent=1: continue as the account openvibe.network already knows (no chooser); if the
++ // network has no session either, it bounces back with error=login_required and we stay quiet.
++ if (req.query.silent) params.set('prompt', 'none');
res.redirect(`${getNetworkBase()}/oauth/authorize?${params.toString()}`);
});
@@@ -308,6 +311,11 @@ router.get('/sso/login', (req, res) => {
router.get('/callback', async (req, res) => {
try {
const { code, state } = req.query;
++ if (!code && req.query.error) {
++ // Silent sign-in found no network session — go home quietly as a guest.
++ res.clearCookie('oauth_state');
++ return res.redirect('/?sso=none');
++ }
if (!code) return res.status(400).send('Missing authorization code');
// Validate CSRF state