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
Changes
nnew file mode 100644@@@ -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>@@@ -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>@@@ -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 uplocalStorage.removeItem('token');localStorage.removeItem('ov_token');@@@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -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>
@@ -5046,7 +5046,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
@@ -5046,7 +5046,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=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-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>
@@ -718,6 +718,11 @@ async function loadUser() {
@@ -718,6 +718,11 @@ async function loadUser() {
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);
} catch (err) {
} 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
localStorage.removeItem('token');
localStorage.removeItem('token');
localStorage.removeItem('ov_token');
localStorage.removeItem('ov_token');
@@ -723,6 +723,30 @@ app.get(['/popout', '/popout/*', '/popout-chat', '/popout-chat/*'], (req, res) =
@@ -723,6 +723,30 @@ app.get(['/popout', '/popout/*', '/popout-chat', '/popout-chat/*'], (req, res) =
res.sendFile(path.join(__dirname, '../public/popout-chat.html'));
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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 ─────────────────────────────────────────────
// ── SPA Fallback ─────────────────────────────────────────────
app.get('*', (req, res) => {
app.get('*', (req, res) => {
// Don't serve HTML for API routes
// Don't serve HTML for API routes