openvibers/openvibe.live Commits
Only the owner lifts a site-wide ban (#878)
Changes
@@@ -183,7 +183,8 @@ router.post('/users/:id/ban', permissions.requireGlobalMod, async (req, res) =>}});--router.delete('/users/:id/ban', permissions.requireGlobalMod, async (req, res) => {++// Lifting a site-wide ban is the owner's alone (the owner, 2026-11-13: "admins can't unban … only owner rank, but they can view").++router.delete('/users/:id/ban', permissions.requireOwner, async (req, res) => {try {const userId = parseInt(req.params.id);const targetUser = await db.getUserById(userId);@@@ -290,8 +291,9 @@ router.delete('/ban/:id', async (req, res) => {// Permission check: global bans require global_mod+, stream bans require stream modif (!ban.stream_id) {-- if (!permissions.isGlobalModOrAbove(req.user)) {-- return res.status(403).json({ error: 'Only global mods can remove global bans' });++ // A site-wide ban is lifted by the owner only (2026-11-13); a stream's own ban stays with its moderators.++ if (!permissions.isOwner(req.user)) {++ return res.status(403).json({ error: 'Only the owner can lift a site-wide ban' });}// Clear is_banned flag if this was a global user banif (ban.user_id) {@@@ -282,7 +282,8 @@ router.post('/users/:id/ban', async (req, res) => {});// ── Unban User ───────────────────────────────────────────────--router.delete('/users/:id/ban', async (req, res) => {++// Lifting a site-wide ban is the owner's alone (the owner, 2026-11-13: "admins can't unban … only owner rank, but they can view").++router.delete('/users/:id/ban', permissions.requireOwner, async (req, res) => {try {await db.run('UPDATE users SET is_banned = 0, ban_reason = NULL WHERE id = ?', [req.params.id]);await db.run('DELETE FROM bans WHERE user_id = ?', [req.params.id]);nnew file mode 100644@@@ -0,0 +1,26 @@++'use strict';++// Lifting a site-wide ban is the owner's alone (the owner, 2026-11-13: "admins can't unban … only owner rank, but they++// can view"). Every route that clears a site ban checks for the owner; a stream's own ban stays with its moderators.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8');++++const mod = read('server/admin/mod-routes.js');++const admin = read('server/admin/routes.js');++assert.ok(/router\.delete\('\/users\/:id\/ban', permissions\.requireOwner, async/.test(mod), 'mod routes: DELETE /users/:id/ban is the owner\'s');++assert.ok(/router\.delete\('\/users\/:id\/ban', permissions\.requireOwner, async/.test(admin), 'admin routes: DELETE /users/:id/ban is the owner\'s');++assert.ok(/if \(!ban\.stream_id\) \{[\s\S]{0,200}if \(!permissions\.isOwner\(req\.user\)\) \{\s*return res\.status\(403\)/.test(mod), 'DELETE /ban/:id: a site-wide ban needs the owner');++assert.ok(!/router\.delete\('\/users\/:id\/ban', permissions\.requireGlobalMod/.test(mod), 'no global mod path left');++++const permissions = require('../server/auth/permissions');++const run = (user) => {++ let status = 200, passed = false;++ const res = { status(s) { status = s; return this; }, json() { return this; } };++ permissions.requireOwner({ user }, res, () => { passed = true; });++ return { status, passed };++};++assert.strictEqual(run({ id: 1, role: 'admin', is_owner: 0 }).passed, false, 'an admin who is not the owner is turned away');++assert.strictEqual(run({ id: 2, role: 'global_mod' }).passed, false, 'a global mod is turned away');++assert.strictEqual(run({ id: 3, role: 'admin', is_owner: 1 }).passed, true, 'the owner may lift it');++console.log('unban-owner-only: only the owner lifts a site-wide ban; admins and mods see the list');
@@ -183,7 +183,8 @@ router.post('/users/:id/ban', permissions.requireGlobalMod, async (req, res) =>
@@ -183,7 +183,8 @@ router.post('/users/:id/ban', permissions.requireGlobalMod, async (req, res) =>
}
}
});
});
-router.delete('/users/:id/ban', permissions.requireGlobalMod, async (req, res) => {
+// Lifting a site-wide ban is the owner's alone (the owner, 2026-11-13: "admins can't unban … only owner rank, but they can view").
+router.delete('/users/:id/ban', permissions.requireOwner, async (req, res) => {
try {
try {
const userId = parseInt(req.params.id);
const userId = parseInt(req.params.id);
const targetUser = await db.getUserById(userId);
const targetUser = await db.getUserById(userId);
@@ -290,8 +291,9 @@ router.delete('/ban/:id', async (req, res) => {
@@ -290,8 +291,9 @@ router.delete('/ban/:id', async (req, res) => {
// Permission check: global bans require global_mod+, stream bans require stream mod
// Permission check: global bans require global_mod+, stream bans require stream mod
if (!ban.stream_id) {
if (!ban.stream_id) {
- if (!permissions.isGlobalModOrAbove(req.user)) {
- return res.status(403).json({ error: 'Only global mods can remove global bans' });
+ // A site-wide ban is lifted by the owner only (2026-11-13); a stream's own ban stays with its moderators.
+ if (!permissions.isOwner(req.user)) {
+ return res.status(403).json({ error: 'Only the owner can lift a site-wide ban' });
}
}
// Clear is_banned flag if this was a global user ban
// Clear is_banned flag if this was a global user ban
if (ban.user_id) {
if (ban.user_id) {
@@ -282,7 +282,8 @@ router.post('/users/:id/ban', async (req, res) => {
@@ -282,7 +282,8 @@ router.post('/users/:id/ban', async (req, res) => {
});
});
// ── Unban User ───────────────────────────────────────────────
// ── Unban User ───────────────────────────────────────────────
-router.delete('/users/:id/ban', async (req, res) => {
+// Lifting a site-wide ban is the owner's alone (the owner, 2026-11-13: "admins can't unban … only owner rank, but they can view").
+router.delete('/users/:id/ban', permissions.requireOwner, async (req, res) => {
try {
try {
await db.run('UPDATE users SET is_banned = 0, ban_reason = NULL WHERE id = ?', [req.params.id]);
await db.run('UPDATE users SET is_banned = 0, ban_reason = NULL WHERE id = ?', [req.params.id]);
await db.run('DELETE FROM bans WHERE user_id = ?', [req.params.id]);
await db.run('DELETE FROM bans WHERE user_id = ?', [req.params.id]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+'use strict';
+// Lifting a site-wide ban is the owner's alone (the owner, 2026-11-13: "admins can't unban … only owner rank, but they
+// can view"). Every route that clears a site ban checks for the owner; a stream's own ban stays with its moderators.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8');
+
+const mod = read('server/admin/mod-routes.js');
+const admin = read('server/admin/routes.js');
+assert.ok(/router\.delete\('\/users\/:id\/ban', permissions\.requireOwner, async/.test(mod), 'mod routes: DELETE /users/:id/ban is the owner\'s');
+assert.ok(/router\.delete\('\/users\/:id\/ban', permissions\.requireOwner, async/.test(admin), 'admin routes: DELETE /users/:id/ban is the owner\'s');
+assert.ok(/if \(!ban\.stream_id\) \{[\s\S]{0,200}if \(!permissions\.isOwner\(req\.user\)\) \{\s*return res\.status\(403\)/.test(mod), 'DELETE /ban/:id: a site-wide ban needs the owner');
+assert.ok(!/router\.delete\('\/users\/:id\/ban', permissions\.requireGlobalMod/.test(mod), 'no global mod path left');
+
+const permissions = require('../server/auth/permissions');
+const run = (user) => {
+ let status = 200, passed = false;
+ const res = { status(s) { status = s; return this; }, json() { return this; } };
+ permissions.requireOwner({ user }, res, () => { passed = true; });
+ return { status, passed };
+};
+assert.strictEqual(run({ id: 1, role: 'admin', is_owner: 0 }).passed, false, 'an admin who is not the owner is turned away');
+assert.strictEqual(run({ id: 2, role: 'global_mod' }).passed, false, 'a global mod is turned away');
+assert.strictEqual(run({ id: 3, role: 'admin', is_owner: 1 }).passed, true, 'the owner may lift it');
+console.log('unban-owner-only: only the owner lifts a site-wide ban; admins and mods see the list');