openvibers/openvibe.network Commits

Names lookup: GET /api/v1/users/names/:name answers { current, network_id, renamed, previous_names } for a current or an old name (banned accounts and unknown names 404), so Live can redirect /@old and pick up a new name it has not seen yet; replaces /api/v1/users/renamed/:name (never deployed)

OP OpenVibers committed 24 Sept 2026 · 3aec618ada · 3 files, +37 −9


Changes

ddiff --git a/server/identity/usernames.js b/server/identity/usernames.js
iindex 2693912a..50f3d6b1 100644
---- a/server/identity/usernames.js
++++ b/server/identity/usernames.js
@@@ -6,7 +6,8 @@
* Users → Rename). Every rename is kept in username_history, so:
* - openvibe.live answers /@old with a 301 to /@new for renamed channels (never for a bare /old,
* which stays a 404: channel URLs are /@username only);
-- * - GET /api/v1/users/renamed/:name tells any site the current name for an old one;
++ * - GET /api/v1/users/names/:name tells any site who holds a name now (current name, Network id,
++ * past names), for an old name and for a new one it has not seen yet;
* - an old name stays reserved for RESERVE_DAYS, so nobody can take it over and inherit the
* person's links, mentions and reputation. Only the person who held it may take it back.
*/
@@@ -78,10 +79,30 @@ function renamedTo(db, oldName) {
return row ? row.username : null;
}
++/**
++ * The public record for a name: { current, network_id, renamed, previous_names } when it is someone's
++ * current name or one they were renamed away from; null otherwise.
++ */
++function lookup(db, name) {
++ ensureSchema(db);
++ if (!NAME_RE.test(String(name || ''))) return null;
++ let user = db.prepare('SELECT id, username FROM users WHERE LOWER(username) = LOWER(?) AND COALESCE(is_banned, 0) = 0').get(name);
++ let renamed = false;
++ if (!user) {
++ const h = db.prepare('SELECT user_id FROM username_history WHERE old_username = ? COLLATE NOCASE ORDER BY id DESC LIMIT 1').get(name);
++ if (!h) return null;
++ user = db.prepare('SELECT id, username FROM users WHERE id = ? AND COALESCE(is_banned, 0) = 0').get(h.user_id);
++ if (!user) return null;
++ renamed = true;
++ }
++ const previous = db.prepare('SELECT old_username FROM username_history WHERE user_id = ? ORDER BY id DESC').all(user.id).map(r => r.old_username);
++ return { current: user.username, network_id: user.id, renamed, previous_names: previous };
++}
++
/** A user's past usernames, newest first. */
function historyOf(db, userId) {
ensureSchema(db);
return db.prepare('SELECT old_username, new_username, changed_at FROM username_history WHERE user_id = ? ORDER BY id DESC').all(userId);
}
--module.exports = { ensureSchema, isReserved, problemWith, rename, renamedTo, historyOf, RESERVE_DAYS, NAME_RE };
++module.exports = { ensureSchema, isReserved, problemWith, rename, renamedTo, lookup, historyOf, RESERVE_DAYS, NAME_RE };
ddiff --git a/server/index.js b/server/index.js
iindex e15c9b0d..d9203da2 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -384,12 +384,13 @@ const jwt = require('jsonwebtoken');
const requireAuth = require('./auth/session').makeRequireAuth(() => ({ db, publicKey, config }), authRoutes.signToken);
// The moderation audit log for staff (staff.moderation.logs): server/admin/moderation-audit.js.
app.use('/api/v1/staff/moderation-audit', requireAuth, moderationAudit.router());
--// Username history (WS-B task 6): the current name for an old one, so sites redirect /@old → /@new.
--app.get('/api/v1/users/renamed/:name', rateLimit({ windowMs: 60_000, max: 120 }), (req, res) => {
-- const to = require('./identity/usernames').renamedTo(db, req.params.name);
-- res.set('Cache-Control', 'public, max-age=300');
-- if (!to) return res.status(404).json({ error: 'not_renamed' });
-- res.json({ username: to });
++// Username history (WS-B task 6): who holds a name now, so sites redirect /@old → /@new and pick up a
++// new name before they have seen it. Banned accounts and unknown names are 404.
++app.get('/api/v1/users/names/:name', rateLimit({ windowMs: 60_000, max: 240 }), (req, res) => {
++ const rec = require('./identity/usernames').lookup(db, req.params.name);
++ res.set('Cache-Control', 'public, max-age=120');
++ if (!rec) return res.status(404).json({ error: 'not_found' });
++ res.json(rec);
});
// The GitHub token (admin → Settings → GitHub, or GITHUB_TOKEN): owner-only admin, and Blog's changelog reads it.
app.use('/api/admin/integrations/github', requireAuth, require('./integrations/github').adminRouter(db));
ddiff --git a/test/usernames.test.js b/test/usernames.test.js
iindex a4bc49ac..6724783d 100644
---- a/test/usernames.test.js
++++ b/test/usernames.test.js
@@@ -51,7 +51,13 @@ try {
assert.ok(/usernames'\)\.isReserved\(db, username\)/.test(auth));
const admin = fs.readFileSync(path.join(__dirname, '../server/admin/routes.js'), 'utf8');
assert.ok(/router\.put\('\/users\/:id\/username'/.test(admin) && /Only the owner renames the owner/.test(admin));
-- assert.ok(/app\.get\('\/api\/v1\/users\/renamed\/:name'/.test(fs.readFileSync(path.join(__dirname, '../server/index.js'), 'utf8')));
++ assert.ok(/app\.get\('\/api\/v1\/users\/names\/:name'/.test(fs.readFileSync(path.join(__dirname, '../server/index.js'), 'utf8')));
++ // The public record: an old name and the current one both lead to the same person.
++ assert.deepStrictEqual(u.lookup(db, 'ann_old'), { current: 'ann_third', network_id: ann, renamed: true, previous_names: ['ann_new', 'ann_old'] });
++ assert.deepStrictEqual(u.lookup(db, 'ANN_THIRD'), { current: 'ann_third', network_id: ann, renamed: false, previous_names: ['ann_new', 'ann_old'] });
++ assert.strictEqual(u.lookup(db, 'nobody_here'), null);
++ db.prepare('UPDATE users SET is_banned = 1 WHERE id = ?').run(ann);
++ assert.strictEqual(u.lookup(db, 'ann_old'), null, 'banned accounts are not looked up');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}