openvibers/openvibe.live Commits

Sign-in: Switch account opens the Network's chooser without signing out; the navbar's accounts and Add another account come back to the page; /api/auth/sso/login passes prompt and login_hint (#366)

OP OpenVibers committed 18 days ago · 3cb6e55435 · 4 files, +66 −8

Changes

ddiff --git a/public/js/app.js b/public/js/app.js
iindex 1872b652..a3eb428d 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -659,13 +659,11 @@ function _checkNetworkSession() {
timer = setTimeout(done, 4000);
}
/** "Switch account": drop this site's session, keep openvibe.network's, open the account chooser. */
++/** Switch account: OpenVibe.Network's chooser (every account this browser holds), back here as the one chosen. Nothing
++ * is signed out first, so closing the chooser keeps this session. */
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';
-- });
++ _setSsoHint('account');
++ location.href = '/api/auth/sso/login?prompt=select_account&next=' + encodeURIComponent(location.pathname + location.search);
}
/** "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 { /* */ } }
ddiff --git a/public/js/ov-navbar-live.js b/public/js/ov-navbar-live.js
iindex 82c75ba9..f7371452 100644
---- a/public/js/ov-navbar-live.js
++++ b/public/js/ov-navbar-live.js
@@@ -69,7 +69,9 @@
return {
service: 'live', mount: '#navbar-mount', className: 'navbar', auth: 'external', apiBase: 'https://openvibe.network',
user: user || null, token: (function () { try { return localStorage.getItem('token'); } catch (e) { return null; } })(),
-- loginUrl: '/api/auth/sso/login', accounts: false, networkLinks: 0, history: false, silentLogin: false, fedcm: false, recent: false,
++ // The menu's other accounts and "Add another account" go through the Network's chooser and come back through
++ // this sign-in (Shared 4.13.0), which returns to the page they were on.
++ loginUrl: '/api/auth/sso/login?next={path}', networkLinks: 0, history: false, silentLogin: false, fedcm: false, recent: false,
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
links: links(user), menu: menu(user),
chips: [
ddiff --git a/server/auth/routes.js b/server/auth/routes.js
iindex af081448..b468fd8a 100644
---- a/server/auth/routes.js
++++ b/server/auth/routes.js
@@@ -3,7 +3,7 @@
* All authentication is via OpenVibe.Tools OAuth2 SSO.
* No local login/register — users sign in at openvibe.network.
*
-- * GET /api/auth/sso/login — Redirect to openvibe.network OAuth
++ * GET /api/auth/sso/login — Redirect to openvibe.network OAuth (?silent=1, ?prompt=select_account|login, ?login_hint, ?next)
* GET /api/auth/callback — OAuth callback
* GET /api/auth/me — Current user
* PUT /api/auth/profile — Update profile
@@@ -319,6 +319,11 @@ router.get('/sso/login', async (req, res) => {
// ?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');
++ // "Switch account" (the chooser) and "use another account" (the sign-in form); login_hint names the account to
++ // continue as when this browser has it (Network docs/sign-in.md).
++ else if (['select_account', 'login'].includes(String(req.query.prompt || ''))) params.set('prompt', String(req.query.prompt));
++ const hint = String(req.query.login_hint || '').trim();
++ if (/^@?[A-Za-z0-9_.-]{1,64}$/.test(hint)) params.set('login_hint', hint);
res.redirect(`${getNetworkBase()}/oauth/authorize?${params.toString()}`);
});
ddiff --git a/test/sso-login-prompt.test.js b/test/sso-login-prompt.test.js
nnew file mode 100644
iindex 00000000..a0c61f00
---- /dev/null
++++ b/test/sso-login-prompt.test.js
@@@ -0,0 +1,53 @@
++'use strict';
++// Live's sign-in (GET /api/auth/sso/login) passes "switch account" and "use another account" on to OpenVibe.Network:
++// ?prompt=select_account shows the chooser, ?prompt=login the sign-in form, ?login_hint=<username> names the account the
++// chooser continues as (owner, 2026-10-26: sign-in "like the Google sign-in does"; Network docs/sign-in.md). ?silent=1
++// stays prompt=none, and anything else is dropped. The menu's "Switch account" opens the chooser without signing out.
++//
++// node test/sso-login-prompt.test.js
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++process.env.NODE_ENV = 'test';
++const quiet = console.log;
++console.log = (...a) => { if (!/^\[/.test(String(a[0]))) quiet(...a); };
++console.warn = () => {};
++
++(async () => {
++ const express = require('express');
++ const cookieParser = require('cookie-parser');
++ const app = express();
++ app.use(cookieParser());
++ app.use('/api/auth', require('../server/auth/routes'));
++ const server = require('http').createServer(app).listen(0);
++ if (!server.listening) await new Promise((r) => server.once('listening', r));
++ const go = async (q) => {
++ const res = await fetch(`http://127.0.0.1:${server.address().port}/api/auth/sso/login${q}`, { redirect: 'manual' });
++ assert.strictEqual(res.status, 302, q);
++ return new URL(res.headers.get('location'));
++ };
++
++ let u = await go('?prompt=select_account&login_hint=%40alt&next=%2Fdashboard');
++ assert.strictEqual(u.pathname, '/oauth/authorize');
++ assert.deepStrictEqual([u.searchParams.get('prompt'), u.searchParams.get('login_hint')], ['select_account', '@alt']);
++ u = await go('?prompt=login');
++ assert.strictEqual(u.searchParams.get('prompt'), 'login');
++ u = await go('?prompt=consent&login_hint=%3Cb%3E');
++ assert.deepStrictEqual([u.searchParams.get('prompt'), u.searchParams.get('login_hint')], [null, null], 'anything else is dropped');
++ u = await go('?silent=1&prompt=login');
++ assert.strictEqual(u.searchParams.get('prompt'), 'none', 'silent stays silent');
++ u = await go('');
++ assert.strictEqual(u.searchParams.get('prompt'), null, 'a plain sign-in: the Network shows its chooser');
++ server.close();
++
++ // The menu's Switch account keeps this session until another account is chosen.
++ const appJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
++ const sw = appJs.slice(appJs.indexOf('function switchAccount()'), appJs.indexOf('function browseAsGuest()'));
++ assert.ok(/prompt=select_account&next=/.test(sw) && !/logout|removeItem/.test(sw), 'Switch account opens the chooser, signing nothing out');
++ const nav = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'ov-navbar-live.js'), 'utf8');
++ assert.ok(/loginUrl: '\/api\/auth\/sso\/login\?next=\{path\}'/.test(nav) && !/accounts: false/.test(nav), "the navbar's accounts come back through this sign-in, to the page they were on");
++
++ quiet('sso login prompt: all checks passed');
++ process.exit(0);
++})().catch((err) => { quiet(err); process.exit(1); });