openvibers/openvibe.live Commits

Ban screen: /banned renders the banned user's name + reason; client sends a banned account there on the 403 from /api/auth/me

OP OpenVibers committed 11 Sept 2026 · bd67aab2d7 · 4 files, +67 −1

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

Changes

ddiff --git a/public/banned.html b/public/banned.html
nnew file mode 100644
iindex 00000000..da2c1a43
---- /dev/null
++++ b/public/banned.html
@@@ -0,0 +1,37 @@
++<!DOCTYPE html>
++<html lang="en">
++<head>
++ <meta charset="UTF-8">
++ <meta name="viewport" content="width=device-width, initial-scale=1.0">
++ <meta name="robots" content="noindex, nofollow">
++ <title>Banned — OpenVibe.Live</title>
++ <link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
++ <style>
++ :root { color-scheme: dark; }
++ * { box-sizing: border-box; }
++ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px 16px; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; color: #e8e6e3;
++ background: radial-gradient(120% 90% at 50% 110%, #3b0a0a 0%, #0d0d0f 60%); }
++ .card { max-width: 640px; width: 100%; text-align: center; padding: 36px 28px; border-radius: 20px; background: rgba(20,12,14,0.85); border: 1px solid rgba(231,76,60,0.45);
++ box-shadow: 0 30px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(231,76,60,0.2), 0 0 60px rgba(231,76,60,0.15); }
++ .stamp { display: inline-block; padding: 8px 22px; border: 4px solid #e74c3c; color: #e74c3c; border-radius: 10px; font-weight: 900; font-size: clamp(2rem, 8vw, 3.4rem); letter-spacing: 0.18em;
++ transform: rotate(-4deg); text-shadow: 0 0 20px rgba(231,76,60,0.5); }
++ h1 { margin: 22px 0 8px; font-size: clamp(1.3rem, 3.6vw, 1.9rem); }
++ p { margin: 0 0 12px; font-size: 1.02rem; line-height: 1.55; color: #cfc9c4; }
++ p b { color: #fff; }
++ .reason { margin: 18px 0; padding: 12px 14px; border-radius: 12px; background: rgba(231,76,60,0.1); border-left: 3px solid #e74c3c; text-align: left; color: #f5d5d0; }
++ .reason small { display: block; font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: #e74c3c; margin-bottom: 4px; }
++ .fine { font-size: 0.82rem; color: #8a8480; margin-top: 18px; }
++ .logo { color: #a78bfa; font-weight: 800; text-decoration: none; }
++ </style>
++</head>
++<body>
++ <div class="card">
++ <div class="stamp">BANNED</div>
++ <h1>{{HEADLINE}}</h1>
++ <p>{{BODY}}</p>
++ <div class="reason"><small>Reason on record</small>{{REASON}}</div>
++ <p><b>This ban is permanent.</b> Don't put any effort into coming back — there is nothing here for you, and this decision won't change.</p>
++ <p class="fine"><span class="logo">OpenVibe.Live</span> · this page is shown to you every time you open the site.</p>
++ </div>
++</body>
++</html>
ddiff --git a/public/index.html b/public/index.html
iindex ab07653c..b3ed9baf 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -5046,7 +5046,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=134" defer></script>
++<script src="/js/app.js?v=135" 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>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex b8447626..b6ee4b9e 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -718,6 +718,11 @@ async function loadUser() {
const data = await api('/auth/me');
currentUser = mergeUserWithCapabilities(data.user || data, data.capabilities);
} 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
localStorage.removeItem('token');
localStorage.removeItem('ov_token');
ddiff --git a/server/index.js b/server/index.js
iindex ae46c48c..d693e608 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -723,6 +723,30 @@ app.get(['/popout', '/popout/*', '/popout-chat', '/popout-chat/*'], (req, res) =
res.sendFile(path.join(__dirname, '../public/popout-chat.html'));
});
++// ── Ban screen ───────────────────────────────────────────────
++// A banned account is sent here by the client the moment /api/auth/me answers 403 (see
++// loadUser in public/js/app.js). The page is rendered with the banned user's name and the
++// reason on record, so they see exactly why every time they open the site.
++app.get('/banned', (req, res) => {
++ const escHtml = (t) => String(t ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
++ let user = null;
++ try {
++ const { extractToken, verifyToken, resolveNetworkUser, authenticateApiToken } = require('./auth/auth');
++ const token = extractToken(req);
++ if (token) { user = authenticateApiToken(token) || (() => { const d = verifyToken(token); return d ? resolveNetworkUser(d) : null; })(); }
++ } catch { user = null; }
++ if (user && !user.is_banned) return res.redirect('/');
++ const name = user ? (user.display_name || user.username) : null;
++ const reason = (user && user.ban_reason) || 'Banned by the site owner.';
++ let html = '';
++ try { html = require('fs').readFileSync(path.join(__dirname, '../public/banned.html'), 'utf8'); } catch { html = '<h1>Banned</h1>'; }
++ html = html
++ .replace('{{HEADLINE}}', name ? `${escHtml(name)}, you are banned from OpenVibe.Live.` : 'This account is banned from OpenVibe.Live.')
++ .replace('{{BODY}}', name ? `You were disrespectful to the person who built and runs this site, and you hurt him. That's the whole story, and it's enough.` : 'The account you signed in with has been banned by the site owner.')
++ .replace('{{REASON}}', escHtml(reason));
++ res.status(403).set('Cache-Control', 'no-store').type('html').send(html);
++});
++
// ── SPA Fallback ─────────────────────────────────────────────
app.get('*', (req, res) => {
// Don't serve HTML for API routes