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
Changes
@@@ -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>@@@ -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">·</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) ── -->@@@ -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 APIfetch('/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 cookieconst 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');@@@ -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><1s latency · chat · emotes · sounds · controls · VOD · clips</small></div>nnew file mode 100644@@@ -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();++})();@@@ -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>@@@ -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>@@@ -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>@@@ -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
@@ -200,7 +200,7 @@
@@ -200,7 +200,7 @@
<header class="topbar">
<header class="topbar">
<a class="topbar-brand" href="https://openvibe.live">
<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>
<span><span class="openvibe">OpenVibe</span>.Live</span>
</a>
</a>
<span class="topbar-sep">/</span>
<span class="topbar-sep">/</span>
@@ -345,5 +345,6 @@
@@ -345,5 +345,6 @@
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
</footer>
</footer>
+<script src="/js/ov-mark.js?v=1"></script>
</body>
</body>
</html>
</html>
@@ -205,6 +205,8 @@
@@ -205,6 +205,8 @@
<a href="#" onclick="event.preventDefault();captureScreenshot()"><i class="fa-solid fa-camera"></i> Screenshot</a>
<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>
<a href="/documentation" onclick="return handleLinkClick(event, '/documentation')"><i class="fa-solid fa-book"></i> Documentation</a>
<hr>
<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>
<a href="#" onclick="event.preventDefault();logout()"><i class="fa-solid fa-right-from-bracket"></i> Logout</a>
</div>
</div>
</div>
</div>
@@ -5038,7 +5040,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
@@ -5038,7 +5040,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════ -->
<footer class="footer">
<footer class="footer">
<div class="footer-content">
<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">·</span>
<span class="footer-divider">·</span>
<a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank"><i class="fa-brands fa-github"></i></a>
<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>
<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
@@ -5086,7 +5088,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<!-- Scripts -->
<!-- Scripts -->
<script src="/js/theme-engine.js?v=4"></script>
<script src="/js/theme-engine.js?v=4"></script>
<script src="/live-notify.js?v=3" defer></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-player.js?v=21" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" 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
@@ -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/pastes.js?v=15" defer></script>
<script src="/js/draggable-fab.js?v=2" defer></script>
<script src="/js/draggable-fab.js?v=2" defer></script>
<script src="/js/messenger.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-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>
<script src="/js/broadcast-guide.js?v=1" defer></script>
<!-- ── OpenVibe shared libs (notifications, account sync, themes) ── -->
<!-- ── OpenVibe shared libs (notifications, account sync, themes) ── -->
@@ -680,7 +680,31 @@ function closeModal() {
@@ -680,7 +680,31 @@ function closeModal() {
function doLogin() { window.location.href = '/api/auth/sso/login'; }
function doLogin() { window.location.href = '/api/auth/sso/login'; }
function doRegister() { 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() {
function logout() {
+ _setSsoHint('guest');
// Clear server-side cookies via API
// Clear server-side cookies via API
fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
// Clear client-side storage (both keys)
// Clear client-side storage (both keys)
@@ -706,7 +730,7 @@ async function loadUser() {
@@ -706,7 +730,7 @@ async function loadUser() {
if (!tok) {
if (!tok) {
// No token in localStorage — try refreshing from httpOnly cookie
// No token in localStorage — try refreshing from httpOnly cookie
const refreshed = await tryRefreshToken();
const refreshed = await tryRefreshToken();
- if (!refreshed) return;
+ if (!refreshed) { _trySilentSso(); return; }
tok = localStorage.getItem('token');
tok = localStorage.getItem('token');
}
}
// If token is already expired, proactively refresh before making the API call
// If token is already expired, proactively refresh before making the API call
@@ -717,17 +741,22 @@ async function loadUser() {
@@ -717,17 +741,22 @@ async function loadUser() {
try {
try {
const data = await api('/auth/me');
const data = await api('/auth/me');
currentUser = mergeUserWithCapabilities(data.user || data, data.capabilities);
currentUser = mergeUserWithCapabilities(data.user || data, data.capabilities);
+ _setSsoHint('account');
} catch (err) {
} catch (err) {
// Banned account: the server refuses with 403 — show the ban screen instead of the site.
// 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 (err && err.status === 403 && /banned/i.test(String(err.message || ''))) {
if (!location.pathname.startsWith('/banned')) location.replace('/banned');
if (!location.pathname.startsWith('/banned')) location.replace('/banned');
return;
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('token');
localStorage.removeItem('ov_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() {
function onAuthChange() {
const anon = document.getElementById('nav-auth-anon');
const anon = document.getElementById('nav-auth-anon');
@@ -50,7 +50,7 @@
@@ -50,7 +50,7 @@
<div class="tour-core-wrap">
<div class="tour-core-wrap">
<div class="tour-core" data-node="core">
<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-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>
<b>OpenVibe.Live</b>
<small><1s latency · chat · emotes · sounds · controls · VOD · clips</small>
<small><1s latency · chat · emotes · sounds · controls · VOD · clips</small>
</div>
</div>
new file mode 100644
new file mode 100644
@@ -0,0 +1,59 @@
@@ -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();
+})();
@@ -519,7 +519,7 @@ button{font:inherit}
@@ -519,7 +519,7 @@ button{font:inherit}
<div class="page">
<div class="page">
<header class="topbar">
<header class="topbar">
<a class="brand" href="https://openvibe.live">
<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>
<span>OPENVIBE NETWORK</span>
</a>
</a>
<div class="network-line">
<div class="network-line">
@@ -1259,5 +1259,6 @@ button{font:inherit}
@@ -1259,5 +1259,6 @@ button{font:inherit}
window.addEventListener("hashchange",maybeAutoOpen);
window.addEventListener("hashchange",maybeAutoOpen);
})();
})();
</script>
</script>
+<script src="/js/ov-mark.js?v=1"></script>
</body>
</body>
</html>
</html>
@@ -196,7 +196,7 @@
@@ -196,7 +196,7 @@
<header class="topbar">
<header class="topbar">
<a class="topbar-brand" href="https://openvibe.live">
<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>
<span><span class="openvibe">OpenVibe</span>.Live</span>
</a>
</a>
<span class="topbar-sep">/</span>
<span class="topbar-sep">/</span>
@@ -322,5 +322,6 @@
@@ -322,5 +322,6 @@
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
</footer>
</footer>
+<script src="/js/ov-mark.js?v=1"></script>
</body>
</body>
</html>
</html>
@@ -196,7 +196,7 @@
@@ -196,7 +196,7 @@
<header class="topbar">
<header class="topbar">
<a class="topbar-brand" href="https://openvibe.live">
<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>
<span><span class="openvibe">OpenVibe</span>.Live</span>
</a>
</a>
<span class="topbar-sep">/</span>
<span class="topbar-sep">/</span>
@@ -359,5 +359,6 @@
@@ -359,5 +359,6 @@
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" rel="noopener"><i class="fa-brands fa-discord"></i> Discord</a>
</footer>
</footer>
+<script src="/js/ov-mark.js?v=1"></script>
</body>
</body>
</html>
</html>
@@ -301,6 +301,9 @@ router.get('/sso/login', (req, res) => {
@@ -301,6 +301,9 @@ router.get('/sso/login', (req, res) => {
scope: 'profile theme',
scope: 'profile theme',
state,
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()}`);
res.redirect(`${getNetworkBase()}/oauth/authorize?${params.toString()}`);
});
});
@@ -308,6 +311,11 @@ router.get('/sso/login', (req, res) => {
@@ -308,6 +311,11 @@ router.get('/sso/login', (req, res) => {
router.get('/callback', async (req, res) => {
router.get('/callback', async (req, res) => {
try {
try {
const { code, state } = req.query;
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');
if (!code) return res.status(400).send('Missing authorization code');
// Validate CSRF state
// Validate CSRF state